qunitx-cli 0.18.0 → 0.19.1
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 +530 -114
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -93,7 +93,7 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
const deadline = Date.now() +
|
|
96
|
+
const deadline = Date.now() + 5e3;
|
|
97
97
|
while (Date.now() < deadline) {
|
|
98
98
|
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(() => true).catch(() => false);
|
|
99
99
|
if (removed) break;
|
|
@@ -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;
|
|
@@ -547,7 +547,8 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
547
547
|
process.stdout.write(`ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # skip
|
|
548
548
|
`);
|
|
549
549
|
} else if (details.status === "todo") {
|
|
550
|
-
|
|
550
|
+
COUNTER.todoCount = (COUNTER.todoCount ?? 0) + 1;
|
|
551
|
+
process.stdout.write(`not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # TODO
|
|
551
552
|
`);
|
|
552
553
|
} else if (details.status === "failed") {
|
|
553
554
|
COUNTER.failCount++;
|
|
@@ -743,8 +744,8 @@ var init_http = __esm({
|
|
|
743
744
|
});
|
|
744
745
|
}
|
|
745
746
|
/** Registers a GET route handler. */
|
|
746
|
-
get(
|
|
747
|
-
this.#registerRouteHandler("GET",
|
|
747
|
+
get(path7, handler) {
|
|
748
|
+
this.#registerRouteHandler("GET", path7, handler);
|
|
748
749
|
}
|
|
749
750
|
/**
|
|
750
751
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -775,30 +776,30 @@ var init_http = __esm({
|
|
|
775
776
|
});
|
|
776
777
|
}
|
|
777
778
|
/** Registers a POST route handler. */
|
|
778
|
-
post(
|
|
779
|
-
this.#registerRouteHandler("POST",
|
|
779
|
+
post(path7, handler) {
|
|
780
|
+
this.#registerRouteHandler("POST", path7, handler);
|
|
780
781
|
}
|
|
781
782
|
/** Registers a DELETE route handler. */
|
|
782
|
-
delete(
|
|
783
|
-
this.#registerRouteHandler("DELETE",
|
|
783
|
+
delete(path7, handler) {
|
|
784
|
+
this.#registerRouteHandler("DELETE", path7, handler);
|
|
784
785
|
}
|
|
785
786
|
/** Registers a PUT route handler. */
|
|
786
|
-
put(
|
|
787
|
-
this.#registerRouteHandler("PUT",
|
|
787
|
+
put(path7, handler) {
|
|
788
|
+
this.#registerRouteHandler("PUT", path7, handler);
|
|
788
789
|
}
|
|
789
790
|
/** Adds a middleware function to the chain. */
|
|
790
791
|
use(middleware) {
|
|
791
792
|
this.middleware.push(middleware);
|
|
792
793
|
}
|
|
793
|
-
#registerRouteHandler(method,
|
|
794
|
+
#registerRouteHandler(method, path7, handler) {
|
|
794
795
|
if (!this.routes[method]) {
|
|
795
796
|
this.routes[method] = {};
|
|
796
797
|
}
|
|
797
|
-
this.routes[method][
|
|
798
|
-
path:
|
|
798
|
+
this.routes[method][path7] = {
|
|
799
|
+
path: path7,
|
|
799
800
|
handler,
|
|
800
|
-
paramNames: this.#extractParamNames(
|
|
801
|
-
isWildcard:
|
|
801
|
+
paramNames: this.#extractParamNames(path7),
|
|
802
|
+
isWildcard: path7 === "/*"
|
|
802
803
|
};
|
|
803
804
|
}
|
|
804
805
|
#handleRequest(req, res) {
|
|
@@ -836,13 +837,13 @@ var init_http = __esm({
|
|
|
836
837
|
return null;
|
|
837
838
|
}
|
|
838
839
|
return routes[url] || Object.values(routes).find((route) => {
|
|
839
|
-
const { path:
|
|
840
|
-
if (!isWildcard && !
|
|
840
|
+
const { path: path7, isWildcard } = route;
|
|
841
|
+
if (!isWildcard && !path7.includes(":")) {
|
|
841
842
|
return false;
|
|
842
843
|
}
|
|
843
|
-
if (isWildcard || this.#matchPathSegments(
|
|
844
|
+
if (isWildcard || this.#matchPathSegments(path7, url)) {
|
|
844
845
|
if (route.paramNames.length > 0) {
|
|
845
|
-
const regexPattern = this.#buildRegexPattern(
|
|
846
|
+
const regexPattern = this.#buildRegexPattern(path7, route.paramNames);
|
|
846
847
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
847
848
|
const regexMatches = regex.exec(url);
|
|
848
849
|
if (regexMatches) {
|
|
@@ -854,8 +855,8 @@ var init_http = __esm({
|
|
|
854
855
|
return false;
|
|
855
856
|
}) || routes["/*"] || null;
|
|
856
857
|
}
|
|
857
|
-
#matchPathSegments(
|
|
858
|
-
const pathSegments =
|
|
858
|
+
#matchPathSegments(path7, url) {
|
|
859
|
+
const pathSegments = path7.split("/");
|
|
859
860
|
const urlSegments = url.split("/");
|
|
860
861
|
if (pathSegments.length !== urlSegments.length) {
|
|
861
862
|
return false;
|
|
@@ -872,14 +873,14 @@ var init_http = __esm({
|
|
|
872
873
|
}
|
|
873
874
|
return true;
|
|
874
875
|
}
|
|
875
|
-
#buildRegexPattern(
|
|
876
|
-
let regexPattern =
|
|
876
|
+
#buildRegexPattern(path7, _paramNames) {
|
|
877
|
+
let regexPattern = path7.replace(/:[^/]+/g, "([^/]+)");
|
|
877
878
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
878
879
|
return regexPattern;
|
|
879
880
|
}
|
|
880
|
-
#extractParamNames(
|
|
881
|
+
#extractParamNames(path7) {
|
|
881
882
|
const paramRegex = /:(\w+)/g;
|
|
882
|
-
const paramMatches =
|
|
883
|
+
const paramMatches = path7.match(paramRegex);
|
|
883
884
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
884
885
|
}
|
|
885
886
|
#extractParams(route, _url) {
|
|
@@ -907,7 +908,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
907
908
|
);
|
|
908
909
|
server.wss.on("connection", function connection(socket) {
|
|
909
910
|
socket.on("message", function message(data) {
|
|
910
|
-
const { event, details, abort } = JSON.parse(data);
|
|
911
|
+
const { event, details, qunitResult, abort } = JSON.parse(data);
|
|
911
912
|
if (event === "wsOpen") {
|
|
912
913
|
config._phase = "loading";
|
|
913
914
|
config._onWsOpen?.();
|
|
@@ -940,6 +941,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
940
941
|
TAPDisplayTestResult(config.COUNTER, details);
|
|
941
942
|
} else if (event === "done") {
|
|
942
943
|
config._phase = "done";
|
|
944
|
+
config._lastQUnitResult = qunitResult ?? null;
|
|
943
945
|
if (config.debug && config._groupMode) {
|
|
944
946
|
process.stdout.write(
|
|
945
947
|
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
@@ -967,7 +969,11 @@ function setupWebServer(config, cachedContent) {
|
|
|
967
969
|
return;
|
|
968
970
|
}
|
|
969
971
|
config._onTestsJsServed?.();
|
|
970
|
-
res.writeHead(200, {
|
|
972
|
+
res.writeHead(200, {
|
|
973
|
+
"Content-Type": "application/javascript",
|
|
974
|
+
"Cache-Control": "no-store",
|
|
975
|
+
"Content-Length": bytes
|
|
976
|
+
});
|
|
971
977
|
res.end(cachedContent.allTestCode);
|
|
972
978
|
});
|
|
973
979
|
server.get("/filtered-tests.js", (_req, res) => {
|
|
@@ -984,14 +990,34 @@ function setupWebServer(config, cachedContent) {
|
|
|
984
990
|
return;
|
|
985
991
|
}
|
|
986
992
|
config._onTestsJsServed?.();
|
|
987
|
-
res.writeHead(200, {
|
|
993
|
+
res.writeHead(200, {
|
|
994
|
+
"Content-Type": "application/javascript",
|
|
995
|
+
"Cache-Control": "no-store",
|
|
996
|
+
"Content-Length": bytes
|
|
997
|
+
});
|
|
988
998
|
res.end(cachedContent.filteredTestCode);
|
|
989
999
|
});
|
|
990
1000
|
server.get("/", async (_req, res) => {
|
|
991
|
-
|
|
1001
|
+
if (cachedContent._buildError) {
|
|
1002
|
+
const htmlContent2 = buildErrorHTML(cachedContent._buildError);
|
|
1003
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1004
|
+
res.write(htmlContent2);
|
|
1005
|
+
res.end();
|
|
1006
|
+
return await fsPromise.writeFile(
|
|
1007
|
+
`${config.projectRoot}/${config.output}/index.html`,
|
|
1008
|
+
htmlContent2
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
if (cachedContent._noTestsWarning) {
|
|
1012
|
+
const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
|
|
1013
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1014
|
+
res.write(htmlContent2);
|
|
1015
|
+
res.end();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
992
1018
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
993
1019
|
mainHTMLWithReplacedAssets,
|
|
994
|
-
|
|
1020
|
+
testRuntimeToInject(config.port, config),
|
|
995
1021
|
"./tests.js"
|
|
996
1022
|
);
|
|
997
1023
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1003,10 +1029,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
1003
1029
|
);
|
|
1004
1030
|
});
|
|
1005
1031
|
server.get("/qunitx.html", async (_req, res) => {
|
|
1006
|
-
|
|
1032
|
+
if (cachedContent._buildError) {
|
|
1033
|
+
const htmlContent2 = buildErrorHTML(cachedContent._buildError);
|
|
1034
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1035
|
+
res.write(htmlContent2);
|
|
1036
|
+
res.end();
|
|
1037
|
+
return await fsPromise.writeFile(
|
|
1038
|
+
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1039
|
+
htmlContent2
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (cachedContent._noTestsWarning) {
|
|
1043
|
+
const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
|
|
1044
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1045
|
+
res.write(htmlContent2);
|
|
1046
|
+
res.end();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1007
1049
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1008
1050
|
mainHTMLWithReplacedAssets,
|
|
1009
|
-
|
|
1051
|
+
testRuntimeToInject(config.port, config),
|
|
1010
1052
|
"./filtered-tests.js"
|
|
1011
1053
|
);
|
|
1012
1054
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1020,10 +1062,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
1020
1062
|
server.get("/*", async (req, res) => {
|
|
1021
1063
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
1022
1064
|
if (possibleDynamicHTML) {
|
|
1023
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
1024
1065
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1025
1066
|
possibleDynamicHTML,
|
|
1026
|
-
|
|
1067
|
+
testRuntimeToInject(config.port, config),
|
|
1027
1068
|
"/tests.js"
|
|
1028
1069
|
);
|
|
1029
1070
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1163,7 +1204,15 @@ function testRuntimeToInject(port, config) {
|
|
|
1163
1204
|
|
|
1164
1205
|
if (!window.QUnit) {
|
|
1165
1206
|
console.log('QUnit not found after WebSocket connected');
|
|
1166
|
-
window.
|
|
1207
|
+
if (window.IS_PLAYWRIGHT) {
|
|
1208
|
+
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1209
|
+
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1210
|
+
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1211
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1212
|
+
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1213
|
+
} else {
|
|
1214
|
+
window.testTimeout = ${config.timeout};
|
|
1215
|
+
}
|
|
1167
1216
|
return;
|
|
1168
1217
|
}
|
|
1169
1218
|
|
|
@@ -1172,11 +1221,6 @@ function testRuntimeToInject(port, config) {
|
|
|
1172
1221
|
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1173
1222
|
}
|
|
1174
1223
|
});
|
|
1175
|
-
window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
|
|
1176
|
-
if (window.IS_PLAYWRIGHT) {
|
|
1177
|
-
window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
|
|
1178
|
-
}
|
|
1179
|
-
});
|
|
1180
1224
|
window.QUnit.on('testStart', (details) => {
|
|
1181
1225
|
window.QUNIT_RESULT.totalTests++;
|
|
1182
1226
|
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
@@ -1196,7 +1240,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1196
1240
|
});
|
|
1197
1241
|
window.QUnit.done((details) => {
|
|
1198
1242
|
if (window.IS_PLAYWRIGHT) {
|
|
1199
|
-
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1243
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1200
1244
|
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1201
1245
|
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
1202
1246
|
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
@@ -1215,6 +1259,211 @@ function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
|
1215
1259
|
return injectScript(html, `${testRuntimeCode}
|
|
1216
1260
|
<script src="${testBundleUrl}" async></script>`);
|
|
1217
1261
|
}
|
|
1262
|
+
function buildNoTestsHTML(files) {
|
|
1263
|
+
const escaped = files.map((f) => f.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")).join("\n");
|
|
1264
|
+
return `<!DOCTYPE html>
|
|
1265
|
+
<html lang="en">
|
|
1266
|
+
<head>
|
|
1267
|
+
<meta charset="utf-8">
|
|
1268
|
+
<meta name="viewport" content="width=device-width">
|
|
1269
|
+
<title>No Tests Registered \u2014 qunitx</title>
|
|
1270
|
+
<style>
|
|
1271
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1272
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1273
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1274
|
+
}
|
|
1275
|
+
#qunit-header {
|
|
1276
|
+
padding: 0.5em 0 0.5em 1em;
|
|
1277
|
+
color: #C2CCD1;
|
|
1278
|
+
background-color: #0D3349;
|
|
1279
|
+
font-size: 1.5em;
|
|
1280
|
+
line-height: 1em;
|
|
1281
|
+
font-weight: 400;
|
|
1282
|
+
border-radius: 5px 5px 0 0;
|
|
1283
|
+
}
|
|
1284
|
+
#qunit-banner { height: 5px; background-color: #F0AD4E; }
|
|
1285
|
+
#qunit-userAgent {
|
|
1286
|
+
padding: 0.5em 1em;
|
|
1287
|
+
color: #fff;
|
|
1288
|
+
background-color: #EC971F;
|
|
1289
|
+
text-shadow: rgba(0,0,0,.3) 2px 2px 1px;
|
|
1290
|
+
font-size: small;
|
|
1291
|
+
}
|
|
1292
|
+
#qunit-tests { list-style: none; font-size: smaller; }
|
|
1293
|
+
#qunit-tests li.warn {
|
|
1294
|
+
display: list-item;
|
|
1295
|
+
padding: 0.4em 1em;
|
|
1296
|
+
border-bottom: 1px solid #fff;
|
|
1297
|
+
color: #000;
|
|
1298
|
+
background-color: #FCF8E3;
|
|
1299
|
+
border-left: 5px solid #F0AD4E;
|
|
1300
|
+
}
|
|
1301
|
+
#qunit-tests li.warn:last-child { border-radius: 0 0 5px 5px; }
|
|
1302
|
+
.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
|
|
1303
|
+
.qunit-assert-list > li {
|
|
1304
|
+
padding: 5px;
|
|
1305
|
+
background-color: #FFF8DC;
|
|
1306
|
+
border-left: 10px solid #F0AD4E;
|
|
1307
|
+
color: #8A6D3B;
|
|
1308
|
+
}
|
|
1309
|
+
.qunit-assert-list pre {
|
|
1310
|
+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
1311
|
+
font-size: 12px;
|
|
1312
|
+
line-height: 1.6;
|
|
1313
|
+
white-space: pre-wrap;
|
|
1314
|
+
word-break: break-word;
|
|
1315
|
+
color: #8A6D3B;
|
|
1316
|
+
margin: 0;
|
|
1317
|
+
}
|
|
1318
|
+
#qunit-testresult {
|
|
1319
|
+
padding: 0.5em 1em;
|
|
1320
|
+
color: #366097;
|
|
1321
|
+
background-color: #E2F0F7;
|
|
1322
|
+
border-bottom: 1px solid #fff;
|
|
1323
|
+
font-size: small;
|
|
1324
|
+
}
|
|
1325
|
+
.dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
|
|
1326
|
+
.dots span:nth-child(2) { animation-delay: .2s; }
|
|
1327
|
+
.dots span:nth-child(3) { animation-delay: .4s; }
|
|
1328
|
+
@keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
|
|
1329
|
+
</style>
|
|
1330
|
+
</head>
|
|
1331
|
+
<body>
|
|
1332
|
+
<div id="qunit">
|
|
1333
|
+
<h1 id="qunit-header">qunitx</h1>
|
|
1334
|
+
<h2 id="qunit-banner"></h2>
|
|
1335
|
+
<div id="qunit-userAgent">Warning: No Tests Registered</div>
|
|
1336
|
+
<ol id="qunit-tests">
|
|
1337
|
+
<li class="warn">
|
|
1338
|
+
<strong>0 QUnit tests were registered in the bundled file(s)</strong>
|
|
1339
|
+
<ol class="qunit-assert-list">
|
|
1340
|
+
<li><pre>${escaped}</pre></li>
|
|
1341
|
+
</ol>
|
|
1342
|
+
</li>
|
|
1343
|
+
</ol>
|
|
1344
|
+
<div id="qunit-testresult">
|
|
1345
|
+
Watching for changes <span class="dots"><span>●</span><span>●</span><span>●</span></span>
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
<script>
|
|
1349
|
+
if (location.port) {
|
|
1350
|
+
(function () {
|
|
1351
|
+
var retries = 0;
|
|
1352
|
+
function connect() {
|
|
1353
|
+
var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
|
|
1354
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1355
|
+
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1356
|
+
ws.addEventListener('error', function () { ws.close(); });
|
|
1357
|
+
}
|
|
1358
|
+
connect();
|
|
1359
|
+
})();
|
|
1360
|
+
}
|
|
1361
|
+
</script>
|
|
1362
|
+
</body>
|
|
1363
|
+
</html>`;
|
|
1364
|
+
}
|
|
1365
|
+
function buildErrorHTML(buildError) {
|
|
1366
|
+
const escaped = buildError.formatted.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1367
|
+
return `<!DOCTYPE html>
|
|
1368
|
+
<html lang="en">
|
|
1369
|
+
<head>
|
|
1370
|
+
<meta charset="utf-8">
|
|
1371
|
+
<meta name="viewport" content="width=device-width">
|
|
1372
|
+
<title>Build Error \u2014 qunitx</title>
|
|
1373
|
+
<style>
|
|
1374
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1375
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1376
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1377
|
+
}
|
|
1378
|
+
#qunit-header {
|
|
1379
|
+
padding: 0.5em 0 0.5em 1em;
|
|
1380
|
+
color: #C2CCD1;
|
|
1381
|
+
background-color: #0D3349;
|
|
1382
|
+
font-size: 1.5em;
|
|
1383
|
+
line-height: 1em;
|
|
1384
|
+
font-weight: 400;
|
|
1385
|
+
border-radius: 5px 5px 0 0;
|
|
1386
|
+
}
|
|
1387
|
+
#qunit-banner { height: 5px; background-color: #EE5757; }
|
|
1388
|
+
#qunit-userAgent {
|
|
1389
|
+
padding: 0.5em 1em;
|
|
1390
|
+
color: #fff;
|
|
1391
|
+
background-color: #2B81AF;
|
|
1392
|
+
text-shadow: rgba(0,0,0,.5) 2px 2px 1px;
|
|
1393
|
+
font-size: small;
|
|
1394
|
+
}
|
|
1395
|
+
#qunit-tests { list-style: none; font-size: smaller; }
|
|
1396
|
+
#qunit-tests li.fail {
|
|
1397
|
+
display: list-item;
|
|
1398
|
+
padding: 0.4em 1em;
|
|
1399
|
+
border-bottom: 1px solid #fff;
|
|
1400
|
+
color: #000;
|
|
1401
|
+
background-color: #EE5757;
|
|
1402
|
+
}
|
|
1403
|
+
#qunit-tests li.fail:last-child { border-radius: 0 0 5px 5px; }
|
|
1404
|
+
.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
|
|
1405
|
+
.qunit-assert-list > li {
|
|
1406
|
+
padding: 5px;
|
|
1407
|
+
background-color: #fff;
|
|
1408
|
+
border-left: 10px solid #EE5757;
|
|
1409
|
+
color: #710909;
|
|
1410
|
+
}
|
|
1411
|
+
.qunit-assert-list pre {
|
|
1412
|
+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
1413
|
+
font-size: 12px;
|
|
1414
|
+
line-height: 1.6;
|
|
1415
|
+
white-space: pre-wrap;
|
|
1416
|
+
word-break: break-word;
|
|
1417
|
+
color: #710909;
|
|
1418
|
+
margin: 0;
|
|
1419
|
+
}
|
|
1420
|
+
#qunit-testresult {
|
|
1421
|
+
padding: 0.5em 1em;
|
|
1422
|
+
color: #366097;
|
|
1423
|
+
background-color: #E2F0F7;
|
|
1424
|
+
border-bottom: 1px solid #fff;
|
|
1425
|
+
font-size: small;
|
|
1426
|
+
}
|
|
1427
|
+
.dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
|
|
1428
|
+
.dots span:nth-child(2) { animation-delay: .2s; }
|
|
1429
|
+
.dots span:nth-child(3) { animation-delay: .4s; }
|
|
1430
|
+
@keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
|
|
1431
|
+
</style>
|
|
1432
|
+
</head>
|
|
1433
|
+
<body>
|
|
1434
|
+
<div id="qunit">
|
|
1435
|
+
<h1 id="qunit-header">qunitx</h1>
|
|
1436
|
+
<h2 id="qunit-banner"></h2>
|
|
1437
|
+
<div id="qunit-userAgent">Build Error: ${buildError.type}</div>
|
|
1438
|
+
<ol id="qunit-tests">
|
|
1439
|
+
<li class="fail">
|
|
1440
|
+
<strong>esbuild failed to bundle test files</strong>
|
|
1441
|
+
<ol class="qunit-assert-list">
|
|
1442
|
+
<li><pre>${escaped}</pre></li>
|
|
1443
|
+
</ol>
|
|
1444
|
+
</li>
|
|
1445
|
+
</ol>
|
|
1446
|
+
<div id="qunit-testresult">
|
|
1447
|
+
Watching for changes <span class="dots"><span>●</span><span>●</span><span>●</span></span>
|
|
1448
|
+
</div>
|
|
1449
|
+
</div>
|
|
1450
|
+
<script>
|
|
1451
|
+
if (location.port) {
|
|
1452
|
+
(function () {
|
|
1453
|
+
var retries = 0;
|
|
1454
|
+
function connect() {
|
|
1455
|
+
var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
|
|
1456
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1457
|
+
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1458
|
+
ws.addEventListener('error', function () { ws.close(); });
|
|
1459
|
+
}
|
|
1460
|
+
connect();
|
|
1461
|
+
})();
|
|
1462
|
+
}
|
|
1463
|
+
</script>
|
|
1464
|
+
</body>
|
|
1465
|
+
</html>`;
|
|
1466
|
+
}
|
|
1218
1467
|
var fsPromise;
|
|
1219
1468
|
var init_web_server = __esm({
|
|
1220
1469
|
"lib/setup/web-server.ts"() {
|
|
@@ -1282,21 +1531,32 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1282
1531
|
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1283
1532
|
const browser = resolvedExistingBrowser || await launchBrowser(config);
|
|
1284
1533
|
const pageStart = Date.now();
|
|
1285
|
-
const
|
|
1534
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1535
|
+
const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
|
|
1536
|
+
const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
|
|
1286
1537
|
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
1287
1538
|
await page.addInitScript(() => {
|
|
1288
1539
|
window.IS_PLAYWRIGHT = true;
|
|
1289
1540
|
});
|
|
1290
|
-
|
|
1541
|
+
config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
|
|
1542
|
+
page.on("console", (msg) => {
|
|
1291
1543
|
const type = msg.type();
|
|
1292
1544
|
const alwaysShow = type === "warning" || type === "error";
|
|
1293
1545
|
if (!alwaysShow && !config.debug) return;
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1546
|
+
const handler = (async () => {
|
|
1547
|
+
try {
|
|
1548
|
+
const values = await Promise.all(
|
|
1549
|
+
msg.args().map(
|
|
1550
|
+
(arg) => arg.jsonValue().catch(() => arg.evaluate((v) => JSON.stringify(v)).then(JSON.parse))
|
|
1551
|
+
)
|
|
1552
|
+
);
|
|
1553
|
+
console.log(...values);
|
|
1554
|
+
} catch {
|
|
1555
|
+
console.log(msg.text());
|
|
1556
|
+
}
|
|
1557
|
+
})();
|
|
1558
|
+
config._pendingConsoleHandlers.add(handler);
|
|
1559
|
+
handler.finally(() => config._pendingConsoleHandlers?.delete(handler));
|
|
1300
1560
|
});
|
|
1301
1561
|
page.on("pageerror", (error) => {
|
|
1302
1562
|
console.error(error.toString());
|
|
@@ -1388,7 +1648,7 @@ var init_run_user_module = __esm({
|
|
|
1388
1648
|
});
|
|
1389
1649
|
|
|
1390
1650
|
// lib/tap/display-final-result.ts
|
|
1391
|
-
function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, timeTaken) {
|
|
1651
|
+
function TAPDisplayFinalResult({ testCount, passCount, skipCount, todoCount, failCount }, timeTaken) {
|
|
1392
1652
|
process.stdout.write("\n");
|
|
1393
1653
|
process.stdout.write(`1..${testCount}
|
|
1394
1654
|
`);
|
|
@@ -1397,6 +1657,8 @@ function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, t
|
|
|
1397
1657
|
process.stdout.write(`# pass ${passCount}
|
|
1398
1658
|
`);
|
|
1399
1659
|
process.stdout.write(`# skip ${skipCount}
|
|
1660
|
+
`);
|
|
1661
|
+
process.stdout.write(`# todo ${todoCount}
|
|
1400
1662
|
`);
|
|
1401
1663
|
process.stdout.write(`# fail ${failCount}
|
|
1402
1664
|
`);
|
|
@@ -1411,7 +1673,36 @@ var init_display_final_result = __esm({
|
|
|
1411
1673
|
|
|
1412
1674
|
// lib/commands/run/tests-in-browser.ts
|
|
1413
1675
|
import fs9 from "node:fs/promises";
|
|
1676
|
+
import path5 from "node:path";
|
|
1414
1677
|
import esbuild from "esbuild";
|
|
1678
|
+
function deriveBuildErrorType(error) {
|
|
1679
|
+
const msgs = error?.errors ?? [];
|
|
1680
|
+
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
1681
|
+
if (/could not resolve|cannot find module|no such file/i.test(text))
|
|
1682
|
+
return "Module Resolution Error";
|
|
1683
|
+
if (/unexpected token|expected .* but found|unterminated/i.test(text)) return "Syntax Error";
|
|
1684
|
+
if (/is not (defined|a function)|cannot read prop/i.test(text)) return "Reference Error";
|
|
1685
|
+
return "Build Error";
|
|
1686
|
+
}
|
|
1687
|
+
function formatBuildErrors(error) {
|
|
1688
|
+
const msgs = error?.errors ?? [];
|
|
1689
|
+
if (msgs.length > 0) {
|
|
1690
|
+
return msgs.map((msg, i) => {
|
|
1691
|
+
const loc = msg.location;
|
|
1692
|
+
const lineNum = loc ? String(loc.line) : "";
|
|
1693
|
+
const pad = loc ? " ".repeat(lineNum.length) : "";
|
|
1694
|
+
const locationLines = loc ? [
|
|
1695
|
+
` ${loc.file}:${loc.line}:${loc.column}`,
|
|
1696
|
+
` ${lineNum} \u2502 ${loc.lineText}`,
|
|
1697
|
+
` ${pad} \u2502 ${" ".repeat(loc.column)}${"~".repeat(Math.max(1, loc.length))}`
|
|
1698
|
+
] : [];
|
|
1699
|
+
const noteLines = msg.notes.filter((n) => n.text).map((n) => ` Note: ${n.text}`);
|
|
1700
|
+
return [`[${i + 1}] ${msg.text}`].concat(locationLines, noteLines).join("\n");
|
|
1701
|
+
}).join("\n\n");
|
|
1702
|
+
}
|
|
1703
|
+
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
1704
|
+
return raw.replace(/\x1b\[[0-9;]*[mGKH]/g, "").replace(/\r\n/g, "\n");
|
|
1705
|
+
}
|
|
1415
1706
|
async function buildTestBundle(config, cachedContent) {
|
|
1416
1707
|
const { projectRoot, output } = config;
|
|
1417
1708
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
@@ -1428,8 +1719,12 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1428
1719
|
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
1429
1720
|
resolveDir: process.cwd()
|
|
1430
1721
|
},
|
|
1722
|
+
// Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
|
|
1723
|
+
// packages from any node_modules on the ancestor chain of cwd — the same lookup
|
|
1724
|
+
// order Node itself uses when resolving require() from process.cwd().
|
|
1725
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
1431
1726
|
bundle: true,
|
|
1432
|
-
logLevel: "
|
|
1727
|
+
logLevel: "silent",
|
|
1433
1728
|
outfile,
|
|
1434
1729
|
keepNames: true,
|
|
1435
1730
|
legalComments: "none",
|
|
@@ -1441,26 +1736,47 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1441
1736
|
// all browsers and does not require changes to user test code.
|
|
1442
1737
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1443
1738
|
};
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1739
|
+
cachedContent._buildError = null;
|
|
1740
|
+
cachedContent._noTestsWarning = null;
|
|
1741
|
+
try {
|
|
1742
|
+
const [allTestCode] = await Promise.all([
|
|
1743
|
+
config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
1744
|
+
Promise.all(
|
|
1745
|
+
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
1746
|
+
const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
|
|
1747
|
+
if (htmlPath !== "/") {
|
|
1748
|
+
await fs9.rm(targetPath, { force: true, recursive: true });
|
|
1749
|
+
await fs9.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
1750
|
+
}
|
|
1751
|
+
})
|
|
1752
|
+
)
|
|
1753
|
+
]);
|
|
1754
|
+
cachedContent.allTestCode = allTestCode;
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
cachedContent._buildError = {
|
|
1757
|
+
type: deriveBuildErrorType(error),
|
|
1758
|
+
formatted: formatBuildErrors(error)
|
|
1759
|
+
};
|
|
1760
|
+
await fs9.writeFile(
|
|
1761
|
+
`${projectRoot}/${output}/index.html`,
|
|
1762
|
+
buildErrorHTML(cachedContent._buildError)
|
|
1763
|
+
);
|
|
1764
|
+
throw error;
|
|
1765
|
+
}
|
|
1457
1766
|
}
|
|
1458
1767
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
1459
1768
|
const { projectRoot, output } = config;
|
|
1460
1769
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
1461
1770
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
1462
1771
|
if (!config._groupMode) {
|
|
1463
|
-
config.COUNTER = {
|
|
1772
|
+
config.COUNTER = {
|
|
1773
|
+
testCount: 0,
|
|
1774
|
+
failCount: 0,
|
|
1775
|
+
skipCount: 0,
|
|
1776
|
+
todoCount: 0,
|
|
1777
|
+
passCount: 0,
|
|
1778
|
+
errorCount: 0
|
|
1779
|
+
};
|
|
1464
1780
|
}
|
|
1465
1781
|
config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
|
|
1466
1782
|
try {
|
|
@@ -1492,11 +1808,26 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1492
1808
|
}
|
|
1493
1809
|
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1494
1810
|
if (!config._groupMode) {
|
|
1811
|
+
if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
|
|
1812
|
+
const displayFiles = allTestFilePaths.map(
|
|
1813
|
+
(f) => f.startsWith(`${projectRoot}/`) ? f.slice(projectRoot.length + 1) : f
|
|
1814
|
+
);
|
|
1815
|
+
cachedContent._noTestsWarning = displayFiles;
|
|
1816
|
+
const fileWord = allTestFilePaths.length === 1 ? "file" : "files";
|
|
1817
|
+
console.log(
|
|
1818
|
+
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
1819
|
+
);
|
|
1820
|
+
fs9.writeFile(`${projectRoot}/${output}/index.html`, buildNoTestsHTML(displayFiles)).catch(
|
|
1821
|
+
() => {
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1495
1825
|
TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
|
|
1496
1826
|
if (config.after) {
|
|
1497
1827
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
1498
1828
|
}
|
|
1499
1829
|
if (!config.watch) {
|
|
1830
|
+
await flushConsoleHandlers(config._pendingConsoleHandlers);
|
|
1500
1831
|
await Promise.all([
|
|
1501
1832
|
connections.server && connections.server.close(),
|
|
1502
1833
|
connections.browser && connections.browser.close()
|
|
@@ -1507,8 +1838,18 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1507
1838
|
}
|
|
1508
1839
|
} catch (error) {
|
|
1509
1840
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1510
|
-
console.log(error);
|
|
1511
1841
|
const exception = new BundleError(error);
|
|
1842
|
+
if (!cachedContent._buildError && error.errors?.length) {
|
|
1843
|
+
cachedContent._buildError = {
|
|
1844
|
+
type: deriveBuildErrorType(error),
|
|
1845
|
+
formatted: formatBuildErrors(error)
|
|
1846
|
+
};
|
|
1847
|
+
fs9.writeFile(
|
|
1848
|
+
`${projectRoot}/${output}/qunitx.html`,
|
|
1849
|
+
buildErrorHTML(cachedContent._buildError)
|
|
1850
|
+
).catch(() => {
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1512
1853
|
if (config.watch) {
|
|
1513
1854
|
console.log(`# ${exception}`);
|
|
1514
1855
|
} else {
|
|
@@ -1526,8 +1867,9 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1526
1867
|
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
1527
1868
|
resolveDir: process.cwd()
|
|
1528
1869
|
},
|
|
1870
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
1529
1871
|
bundle: true,
|
|
1530
|
-
logLevel: "
|
|
1872
|
+
logLevel: "silent",
|
|
1531
1873
|
outfile: outputPath,
|
|
1532
1874
|
legalComments: "none",
|
|
1533
1875
|
target: esbuildTarget(config.browser),
|
|
@@ -1542,15 +1884,13 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
1542
1884
|
const MAX_RETRIES = 3;
|
|
1543
1885
|
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1544
1886
|
let { result, js } = await getContents();
|
|
1887
|
+
const initialSize = js.length;
|
|
1545
1888
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
1546
1889
|
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
1547
|
-
console.log(
|
|
1548
|
-
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes (< ${EMPTY_BUNDLE_THRESHOLD}) on attempt ${retry}/${MAX_RETRIES} \u2014 overlayfs flush race, retrying in ${RETRY_DELAY_MS}ms`
|
|
1549
|
-
);
|
|
1550
1890
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
1551
1891
|
({ result, js } = await getContents());
|
|
1552
1892
|
}
|
|
1553
|
-
if (js.length < EMPTY_BUNDLE_THRESHOLD) {
|
|
1893
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
|
|
1554
1894
|
console.log(
|
|
1555
1895
|
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
1556
1896
|
);
|
|
@@ -1629,7 +1969,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1629
1969
|
clearTimeout(timeoutHandle);
|
|
1630
1970
|
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1631
1971
|
await testRaceResult;
|
|
1632
|
-
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
1972
|
+
QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
|
|
1633
1973
|
} catch (error) {
|
|
1634
1974
|
targetError = error;
|
|
1635
1975
|
console.log(error);
|
|
@@ -1640,14 +1980,22 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1640
1980
|
config._onTestsJsServed = null;
|
|
1641
1981
|
config._resetTestTimeout = null;
|
|
1642
1982
|
config._testRunDone = null;
|
|
1983
|
+
config._lastQUnitResult = null;
|
|
1643
1984
|
}
|
|
1644
|
-
if (!QUNIT_RESULT
|
|
1985
|
+
if (!QUNIT_RESULT) {
|
|
1645
1986
|
if (targetError) console.log(targetError);
|
|
1646
1987
|
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";
|
|
1647
1988
|
console.log(`# TIMEOUT: ${wsReason}`);
|
|
1648
1989
|
console.log("BROWSER: runtime error thrown during executing tests");
|
|
1649
1990
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
1650
|
-
await failOnNonWatchMode(
|
|
1991
|
+
await failOnNonWatchMode(
|
|
1992
|
+
config.watch,
|
|
1993
|
+
{ server, browser },
|
|
1994
|
+
config._groupMode,
|
|
1995
|
+
config._pendingConsoleHandlers
|
|
1996
|
+
);
|
|
1997
|
+
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
1998
|
+
return;
|
|
1651
1999
|
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
1652
2000
|
if (targetError) console.log(targetError);
|
|
1653
2001
|
console.log(
|
|
@@ -1655,16 +2003,22 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1655
2003
|
);
|
|
1656
2004
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1657
2005
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1658
|
-
await failOnNonWatchMode(
|
|
2006
|
+
await failOnNonWatchMode(
|
|
2007
|
+
config.watch,
|
|
2008
|
+
{ server, browser },
|
|
2009
|
+
config._groupMode,
|
|
2010
|
+
config._pendingConsoleHandlers
|
|
2011
|
+
);
|
|
1659
2012
|
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
1660
2013
|
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
1661
2014
|
}
|
|
1662
2015
|
}
|
|
1663
|
-
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
|
|
2016
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
1664
2017
|
if (!watchMode) {
|
|
1665
2018
|
if (groupMode) {
|
|
1666
2019
|
throw new Error("Browser test run failed");
|
|
1667
2020
|
}
|
|
2021
|
+
await flushConsoleHandlers(pendingHandlers);
|
|
1668
2022
|
await Promise.all([
|
|
1669
2023
|
connections.server && connections.server.close(),
|
|
1670
2024
|
connections.browser && connections.browser.close()
|
|
@@ -1673,7 +2027,12 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
1673
2027
|
process.exit(1);
|
|
1674
2028
|
}
|
|
1675
2029
|
}
|
|
1676
|
-
|
|
2030
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + 2e3) {
|
|
2031
|
+
if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
|
|
2032
|
+
await Promise.allSettled([...handlers]);
|
|
2033
|
+
return flushConsoleHandlers(handlers, deadline);
|
|
2034
|
+
}
|
|
2035
|
+
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES;
|
|
1677
2036
|
var init_tests_in_browser = __esm({
|
|
1678
2037
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
1679
2038
|
init_color();
|
|
@@ -1681,6 +2040,7 @@ var init_tests_in_browser = __esm({
|
|
|
1681
2040
|
init_time_counter();
|
|
1682
2041
|
init_run_user_module();
|
|
1683
2042
|
init_display_final_result();
|
|
2043
|
+
init_web_server();
|
|
1684
2044
|
BundleError = class extends Error {
|
|
1685
2045
|
constructor(message) {
|
|
1686
2046
|
super(message);
|
|
@@ -1688,13 +2048,17 @@ var init_tests_in_browser = __esm({
|
|
|
1688
2048
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
1689
2049
|
}
|
|
1690
2050
|
};
|
|
2051
|
+
ancestorNodeModules = (dir) => dir.split(path5.sep).map(
|
|
2052
|
+
(_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
|
|
2053
|
+
);
|
|
2054
|
+
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
1691
2055
|
}
|
|
1692
2056
|
});
|
|
1693
2057
|
|
|
1694
2058
|
// lib/setup/file-watcher.ts
|
|
1695
2059
|
import fs10 from "node:fs";
|
|
1696
2060
|
import { stat, lstat } from "node:fs/promises";
|
|
1697
|
-
import
|
|
2061
|
+
import path6 from "node:path";
|
|
1698
2062
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1699
2063
|
const extensions = config.extensions || ["js", "ts"];
|
|
1700
2064
|
const readyPromises = [];
|
|
@@ -1724,7 +2088,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1724
2088
|
const lastChangeMs = {};
|
|
1725
2089
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1726
2090
|
if (!ready || !filename) return;
|
|
1727
|
-
const fullPath =
|
|
2091
|
+
const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
|
|
1728
2092
|
if (eventType === "change") {
|
|
1729
2093
|
if (!config._building) {
|
|
1730
2094
|
const now = Date.now();
|
|
@@ -1749,8 +2113,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1749
2113
|
}
|
|
1750
2114
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
1751
2115
|
});
|
|
1752
|
-
const parentDir =
|
|
1753
|
-
const watchedBasename =
|
|
2116
|
+
const parentDir = path6.dirname(watchPath);
|
|
2117
|
+
const watchedBasename = path6.basename(watchPath);
|
|
1754
2118
|
let parentUnlinkFired = false;
|
|
1755
2119
|
const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
|
|
1756
2120
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
@@ -1825,7 +2189,8 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
1825
2189
|
"#",
|
|
1826
2190
|
magenta().bold("==================================================================")
|
|
1827
2191
|
);
|
|
1828
|
-
|
|
2192
|
+
const displayPath = filePath.startsWith(config.projectRoot) ? filePath.slice(config.projectRoot.length) : filePath;
|
|
2193
|
+
console.log("#", colorEvent(event), displayPath);
|
|
1829
2194
|
console.log(
|
|
1830
2195
|
"#",
|
|
1831
2196
|
magenta().bold("==================================================================")
|
|
@@ -1852,13 +2217,13 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
1852
2217
|
}
|
|
1853
2218
|
});
|
|
1854
2219
|
}
|
|
1855
|
-
function mutateFSTree(fsTree, event,
|
|
2220
|
+
function mutateFSTree(fsTree, event, path7) {
|
|
1856
2221
|
if (event === "add") {
|
|
1857
|
-
fsTree[
|
|
2222
|
+
fsTree[path7] = null;
|
|
1858
2223
|
} else if (event === "unlink") {
|
|
1859
|
-
delete fsTree[
|
|
2224
|
+
delete fsTree[path7];
|
|
1860
2225
|
} else if (event === "unlinkDir") {
|
|
1861
|
-
const dirPrefix =
|
|
2226
|
+
const dirPrefix = path7.endsWith("/") ? path7 : path7 + "/";
|
|
1862
2227
|
for (const treePath of Object.keys(fsTree)) {
|
|
1863
2228
|
if (treePath.startsWith(dirPrefix)) delete fsTree[treePath];
|
|
1864
2229
|
}
|
|
@@ -1873,7 +2238,7 @@ var CHANGE_DEDUPE_MS;
|
|
|
1873
2238
|
var init_file_watcher = __esm({
|
|
1874
2239
|
"lib/setup/file-watcher.ts"() {
|
|
1875
2240
|
init_color();
|
|
1876
|
-
CHANGE_DEDUPE_MS =
|
|
2241
|
+
CHANGE_DEDUPE_MS = 10;
|
|
1877
2242
|
}
|
|
1878
2243
|
});
|
|
1879
2244
|
|
|
@@ -1990,14 +2355,18 @@ import { availableParallelism } from "node:os";
|
|
|
1990
2355
|
async function run(config) {
|
|
1991
2356
|
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
1992
2357
|
if (config.watch) {
|
|
1993
|
-
|
|
2358
|
+
const preBuildPromise = buildTestBundle(config, cachedContent);
|
|
2359
|
+
preBuildPromise.catch(() => {
|
|
2360
|
+
});
|
|
2361
|
+
cachedContent._preBuildPromise = preBuildPromise;
|
|
1994
2362
|
const [connections] = await Promise.all([
|
|
1995
2363
|
setupBrowser(config, cachedContent),
|
|
1996
2364
|
writeOutputStaticFiles(config, cachedContent)
|
|
1997
2365
|
]);
|
|
1998
2366
|
config.expressApp = connections.server;
|
|
1999
2367
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
2000
|
-
|
|
2368
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2369
|
+
if (config.open && !isHeadedWatchMode) {
|
|
2001
2370
|
void openOutputInBrowser(config);
|
|
2002
2371
|
}
|
|
2003
2372
|
if (config.before) {
|
|
@@ -2012,6 +2381,10 @@ async function run(config) {
|
|
|
2012
2381
|
]);
|
|
2013
2382
|
throw error;
|
|
2014
2383
|
}
|
|
2384
|
+
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2385
|
+
await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2015
2388
|
if (config.watch) {
|
|
2016
2389
|
const { ready: watcherReady } = setupFileWatchers(
|
|
2017
2390
|
config.testFileLookupPaths,
|
|
@@ -2035,7 +2408,13 @@ async function run(config) {
|
|
|
2035
2408
|
}
|
|
2036
2409
|
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
2037
2410
|
},
|
|
2038
|
-
(_path, _event) =>
|
|
2411
|
+
async (_path, _event) => {
|
|
2412
|
+
connections.server.publish("refresh");
|
|
2413
|
+
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2414
|
+
await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2039
2418
|
);
|
|
2040
2419
|
await watcherReady;
|
|
2041
2420
|
}
|
|
@@ -2043,8 +2422,15 @@ async function run(config) {
|
|
|
2043
2422
|
} else {
|
|
2044
2423
|
const allFiles = Object.keys(config.fsTree);
|
|
2045
2424
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2046
|
-
const groups = splitIntoGroups(allFiles, groupCount);
|
|
2047
|
-
config.COUNTER = {
|
|
2425
|
+
const groups = await splitIntoGroups(allFiles, groupCount);
|
|
2426
|
+
config.COUNTER = {
|
|
2427
|
+
testCount: 0,
|
|
2428
|
+
failCount: 0,
|
|
2429
|
+
skipCount: 0,
|
|
2430
|
+
todoCount: 0,
|
|
2431
|
+
passCount: 0,
|
|
2432
|
+
errorCount: 0
|
|
2433
|
+
};
|
|
2048
2434
|
config.lastRanTestFiles = allFiles;
|
|
2049
2435
|
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
2050
2436
|
...config,
|
|
@@ -2105,6 +2491,7 @@ async function run(config) {
|
|
|
2105
2491
|
try {
|
|
2106
2492
|
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
2107
2493
|
} finally {
|
|
2494
|
+
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
2108
2495
|
await Promise.all([
|
|
2109
2496
|
connections.server && connections.server.close(),
|
|
2110
2497
|
connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
@@ -2133,6 +2520,12 @@ async function run(config) {
|
|
|
2133
2520
|
config.COUNTER.failCount > 0 ? 1 : 0
|
|
2134
2521
|
);
|
|
2135
2522
|
process.exitCode = exitCode;
|
|
2523
|
+
if (config.COUNTER.testCount === 0 && exitCode === 0) {
|
|
2524
|
+
const fileWord = allFiles.length === 1 ? "file" : "files";
|
|
2525
|
+
console.log(
|
|
2526
|
+
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allFiles.length} ${fileWord}`
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2136
2529
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
2137
2530
|
if (config.after) {
|
|
2138
2531
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
@@ -2204,10 +2597,24 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
2204
2597
|
}
|
|
2205
2598
|
return cachedContent;
|
|
2206
2599
|
}
|
|
2207
|
-
function splitIntoGroups(files, groupCount) {
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2600
|
+
async function splitIntoGroups(files, groupCount) {
|
|
2601
|
+
const withSizes = await Promise.all(
|
|
2602
|
+
files.map(
|
|
2603
|
+
(f) => fs12.stat(f).then(({ size }) => ({ f, size })).catch(() => ({ f, size: 0 }))
|
|
2604
|
+
)
|
|
2605
|
+
);
|
|
2606
|
+
return withSizes.sort((a, b) => b.size - a.size).reduce(
|
|
2607
|
+
(groups, { f, size }) => {
|
|
2608
|
+
const { idx } = groups.reduce(
|
|
2609
|
+
(min, { total }, i) => total < min.total ? { idx: i, total } : min,
|
|
2610
|
+
{ idx: 0, total: groups[0].total }
|
|
2611
|
+
);
|
|
2612
|
+
groups[idx].files.push(f);
|
|
2613
|
+
groups[idx].total += size;
|
|
2614
|
+
return groups;
|
|
2615
|
+
},
|
|
2616
|
+
Array.from({ length: groupCount }, () => ({ files: [], total: 0 }))
|
|
2617
|
+
).flatMap((g) => g.files.length > 0 ? [g.files] : []);
|
|
2211
2618
|
}
|
|
2212
2619
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
2213
2620
|
const prefix = "Watching files...";
|
|
@@ -2256,7 +2663,7 @@ init_color();
|
|
|
2256
2663
|
var package_default = {
|
|
2257
2664
|
name: "qunitx-cli",
|
|
2258
2665
|
type: "module",
|
|
2259
|
-
version: "0.
|
|
2666
|
+
version: "0.19.1",
|
|
2260
2667
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2261
2668
|
author: "Izel Nakri",
|
|
2262
2669
|
license: "MIT",
|
|
@@ -2488,17 +2895,17 @@ function pathToModuleName(filePath) {
|
|
|
2488
2895
|
async function generateTestFiles() {
|
|
2489
2896
|
const projectRoot = await findProjectRoot();
|
|
2490
2897
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
2491
|
-
const
|
|
2492
|
-
if (await pathExists(
|
|
2493
|
-
console.log(`${
|
|
2898
|
+
const path7 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
2899
|
+
if (await pathExists(path7)) {
|
|
2900
|
+
console.log(`${path7} already exists!`);
|
|
2494
2901
|
return;
|
|
2495
2902
|
}
|
|
2496
2903
|
const testJSContent = await readTemplate("test.js");
|
|
2497
|
-
const targetFolderPaths =
|
|
2904
|
+
const targetFolderPaths = path7.split("/");
|
|
2498
2905
|
targetFolderPaths.pop();
|
|
2499
2906
|
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
2500
|
-
await fs5.writeFile(
|
|
2501
|
-
console.log(green(`${
|
|
2907
|
+
await fs5.writeFile(path7, testJSContent.replace("{{moduleName}}", moduleName));
|
|
2908
|
+
console.log(green(`${path7} written`));
|
|
2502
2909
|
}
|
|
2503
2910
|
|
|
2504
2911
|
// lib/setup/config.ts
|
|
@@ -2599,20 +3006,20 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
2599
3006
|
});
|
|
2600
3007
|
return result.map((metaItem) => metaItem.input);
|
|
2601
3008
|
}
|
|
2602
|
-
function pathIsFile(
|
|
2603
|
-
const inputs2 =
|
|
3009
|
+
function pathIsFile(path7) {
|
|
3010
|
+
const inputs2 = path7.split("/");
|
|
2604
3011
|
return inputs2[inputs2.length - 1].includes(".");
|
|
2605
3012
|
}
|
|
2606
3013
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
2607
|
-
return paths.some((
|
|
2608
|
-
if (
|
|
3014
|
+
return paths.some((path7) => {
|
|
3015
|
+
if (path7 === targetPath) {
|
|
2609
3016
|
return false;
|
|
2610
3017
|
}
|
|
2611
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
3018
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path7));
|
|
2612
3019
|
});
|
|
2613
3020
|
}
|
|
2614
|
-
function buildGlobFormat(
|
|
2615
|
-
return
|
|
3021
|
+
function buildGlobFormat(path7) {
|
|
3022
|
+
return path7.isFile ? path7.input : `${path7.input}/**`;
|
|
2616
3023
|
}
|
|
2617
3024
|
|
|
2618
3025
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -2666,7 +3073,9 @@ function parseCliFlags(projectRoot) {
|
|
|
2666
3073
|
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
2667
3074
|
return result;
|
|
2668
3075
|
}
|
|
2669
|
-
result.inputs.add(
|
|
3076
|
+
result.inputs.add(
|
|
3077
|
+
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : `${process.cwd()}/${arg}`
|
|
3078
|
+
);
|
|
2670
3079
|
return result;
|
|
2671
3080
|
},
|
|
2672
3081
|
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
@@ -2704,7 +3113,14 @@ async function setupConfig() {
|
|
|
2704
3113
|
testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
|
|
2705
3114
|
lastFailedTestFiles: null,
|
|
2706
3115
|
lastRanTestFiles: null,
|
|
2707
|
-
COUNTER: {
|
|
3116
|
+
COUNTER: {
|
|
3117
|
+
testCount: 0,
|
|
3118
|
+
failCount: 0,
|
|
3119
|
+
skipCount: 0,
|
|
3120
|
+
todoCount: 0,
|
|
3121
|
+
passCount: 0,
|
|
3122
|
+
errorCount: 0
|
|
3123
|
+
},
|
|
2708
3124
|
_testRunDone: null,
|
|
2709
3125
|
_resetTestTimeout: null,
|
|
2710
3126
|
_onWsOpen: null,
|