qunitx-cli 0.19.0 → 0.19.2
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 +237 -117
- 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;
|
|
@@ -385,21 +385,6 @@ var init_color = __esm({
|
|
|
385
385
|
}
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
-
// lib/utils/path-exists.ts
|
|
389
|
-
import fs2 from "node:fs/promises";
|
|
390
|
-
async function pathExists(path7) {
|
|
391
|
-
try {
|
|
392
|
-
await fs2.access(path7);
|
|
393
|
-
return true;
|
|
394
|
-
} catch {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
var init_path_exists = __esm({
|
|
399
|
-
"lib/utils/path-exists.ts"() {
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
388
|
// lib/utils/read-template.ts
|
|
404
389
|
import fs3 from "node:fs/promises";
|
|
405
390
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -906,9 +891,10 @@ function setupWebServer(config, cachedContent) {
|
|
|
906
891
|
cachedContent.mainHTML.filePath,
|
|
907
892
|
config.projectRoot
|
|
908
893
|
);
|
|
894
|
+
const runtimeScript = testRuntimeToInject(config);
|
|
909
895
|
server.wss.on("connection", function connection(socket) {
|
|
910
896
|
socket.on("message", function message(data) {
|
|
911
|
-
const { event, details, abort } = JSON.parse(data);
|
|
897
|
+
const { event, details, qunitResult, abort } = JSON.parse(data);
|
|
912
898
|
if (event === "wsOpen") {
|
|
913
899
|
config._phase = "loading";
|
|
914
900
|
config._onWsOpen?.();
|
|
@@ -941,6 +927,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
941
927
|
TAPDisplayTestResult(config.COUNTER, details);
|
|
942
928
|
} else if (event === "done") {
|
|
943
929
|
config._phase = "done";
|
|
930
|
+
config._lastQUnitResult = qunitResult ?? null;
|
|
944
931
|
if (config.debug && config._groupMode) {
|
|
945
932
|
process.stdout.write(
|
|
946
933
|
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
@@ -956,7 +943,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
956
943
|
});
|
|
957
944
|
server.get("/tests.js", (_req, res) => {
|
|
958
945
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
959
|
-
process.stdout.write(
|
|
946
|
+
config.debug && process.stdout.write(
|
|
960
947
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
961
948
|
`
|
|
962
949
|
);
|
|
@@ -968,12 +955,16 @@ function setupWebServer(config, cachedContent) {
|
|
|
968
955
|
return;
|
|
969
956
|
}
|
|
970
957
|
config._onTestsJsServed?.();
|
|
971
|
-
res.writeHead(200, {
|
|
958
|
+
res.writeHead(200, {
|
|
959
|
+
"Content-Type": "application/javascript",
|
|
960
|
+
"Cache-Control": "no-store",
|
|
961
|
+
"Content-Length": bytes
|
|
962
|
+
});
|
|
972
963
|
res.end(cachedContent.allTestCode);
|
|
973
964
|
});
|
|
974
965
|
server.get("/filtered-tests.js", (_req, res) => {
|
|
975
966
|
const bytes = cachedContent.filteredTestCode?.length ?? null;
|
|
976
|
-
process.stdout.write(
|
|
967
|
+
config.debug && process.stdout.write(
|
|
977
968
|
`# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
|
|
978
969
|
`
|
|
979
970
|
);
|
|
@@ -985,7 +976,11 @@ function setupWebServer(config, cachedContent) {
|
|
|
985
976
|
return;
|
|
986
977
|
}
|
|
987
978
|
config._onTestsJsServed?.();
|
|
988
|
-
res.writeHead(200, {
|
|
979
|
+
res.writeHead(200, {
|
|
980
|
+
"Content-Type": "application/javascript",
|
|
981
|
+
"Cache-Control": "no-store",
|
|
982
|
+
"Content-Length": bytes
|
|
983
|
+
});
|
|
989
984
|
res.end(cachedContent.filteredTestCode);
|
|
990
985
|
});
|
|
991
986
|
server.get("/", async (_req, res) => {
|
|
@@ -1006,10 +1001,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
1006
1001
|
res.end();
|
|
1007
1002
|
return;
|
|
1008
1003
|
}
|
|
1009
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
1010
1004
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1011
1005
|
mainHTMLWithReplacedAssets,
|
|
1012
|
-
|
|
1006
|
+
runtimeScript,
|
|
1013
1007
|
"./tests.js"
|
|
1014
1008
|
);
|
|
1015
1009
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1038,10 +1032,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
1038
1032
|
res.end();
|
|
1039
1033
|
return;
|
|
1040
1034
|
}
|
|
1041
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
1042
1035
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1043
1036
|
mainHTMLWithReplacedAssets,
|
|
1044
|
-
|
|
1037
|
+
runtimeScript,
|
|
1045
1038
|
"./filtered-tests.js"
|
|
1046
1039
|
);
|
|
1047
1040
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1055,10 +1048,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
1055
1048
|
server.get("/*", async (req, res) => {
|
|
1056
1049
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
1057
1050
|
if (possibleDynamicHTML) {
|
|
1058
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
1059
1051
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1060
1052
|
possibleDynamicHTML,
|
|
1061
|
-
|
|
1053
|
+
runtimeScript,
|
|
1062
1054
|
"/tests.js"
|
|
1063
1055
|
);
|
|
1064
1056
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1070,21 +1062,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
1070
1062
|
);
|
|
1071
1063
|
}
|
|
1072
1064
|
const url = req.url;
|
|
1073
|
-
const requestStartedAt =
|
|
1065
|
+
const requestStartedAt = Date.now();
|
|
1074
1066
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1067
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1068
|
+
const stream = fs8.createReadStream(filePath);
|
|
1069
|
+
stream.on("open", () => {
|
|
1070
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1071
|
+
stream.pipe(res);
|
|
1072
|
+
config.debug && process.stdout.write(
|
|
1073
|
+
`# [HTTPServer] GET ${url} 200 - ${Date.now() - requestStartedAt}ms
|
|
1074
|
+
`
|
|
1075
|
+
);
|
|
1078
1076
|
});
|
|
1079
|
-
|
|
1080
|
-
res.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
process.stdout.write(
|
|
1085
|
-
`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms
|
|
1077
|
+
stream.on("error", () => {
|
|
1078
|
+
res.writeHead(404, { "Content-Type": contentType });
|
|
1079
|
+
res.end(contentType === MIME_TYPES.html ? NOT_FOUND_HTML : void 0);
|
|
1080
|
+
config.debug && process.stdout.write(
|
|
1081
|
+
`# [HTTPServer] GET ${url} 404 - ${Date.now() - requestStartedAt}ms
|
|
1086
1082
|
`
|
|
1087
|
-
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1088
1085
|
});
|
|
1089
1086
|
return server;
|
|
1090
1087
|
}
|
|
@@ -1096,7 +1093,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1096
1093
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1097
1094
|
}, html);
|
|
1098
1095
|
}
|
|
1099
|
-
function testRuntimeToInject(
|
|
1096
|
+
function testRuntimeToInject(config) {
|
|
1100
1097
|
return `<script>
|
|
1101
1098
|
window.testTimeout = 0;
|
|
1102
1099
|
setInterval(() => {
|
|
@@ -1132,7 +1129,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1132
1129
|
|
|
1133
1130
|
function setupWebSocket() {
|
|
1134
1131
|
try {
|
|
1135
|
-
window.socket = new WebSocket(
|
|
1132
|
+
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1136
1133
|
} catch (error) {
|
|
1137
1134
|
console.log(error);
|
|
1138
1135
|
retryOrFail();
|
|
@@ -1144,7 +1141,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1144
1141
|
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1145
1142
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1146
1143
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1147
|
-
if (
|
|
1144
|
+
if (navigator.webdriver) {
|
|
1148
1145
|
window.socket.send(JSON.stringify({ event: 'wsOpen' }));
|
|
1149
1146
|
}
|
|
1150
1147
|
maybeStart();
|
|
@@ -1153,9 +1150,9 @@ function testRuntimeToInject(port, config) {
|
|
|
1153
1150
|
retryOrFail();
|
|
1154
1151
|
});
|
|
1155
1152
|
window.socket.addEventListener('message', function(messageEvent) {
|
|
1156
|
-
if (!
|
|
1153
|
+
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1157
1154
|
window.location.reload(true);
|
|
1158
|
-
} else if (
|
|
1155
|
+
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1159
1156
|
window.abortQUnit = true;
|
|
1160
1157
|
window.QUnit.config.queue.length = 0;
|
|
1161
1158
|
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
@@ -1198,7 +1195,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1198
1195
|
|
|
1199
1196
|
if (!window.QUnit) {
|
|
1200
1197
|
console.log('QUnit not found after WebSocket connected');
|
|
1201
|
-
if (
|
|
1198
|
+
if (navigator.webdriver) {
|
|
1202
1199
|
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1203
1200
|
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1204
1201
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
@@ -1211,15 +1208,10 @@ function testRuntimeToInject(port, config) {
|
|
|
1211
1208
|
}
|
|
1212
1209
|
|
|
1213
1210
|
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1214
|
-
if (
|
|
1211
|
+
if (navigator.webdriver) {
|
|
1215
1212
|
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1216
1213
|
}
|
|
1217
1214
|
});
|
|
1218
|
-
window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
|
|
1219
|
-
if (window.IS_PLAYWRIGHT) {
|
|
1220
|
-
window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
1215
|
window.QUnit.on('testStart', (details) => {
|
|
1224
1216
|
window.QUNIT_RESULT.totalTests++;
|
|
1225
1217
|
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
@@ -1229,8 +1221,10 @@ function testRuntimeToInject(port, config) {
|
|
|
1229
1221
|
window.QUNIT_RESULT.finishedTests++;
|
|
1230
1222
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1231
1223
|
window.QUNIT_RESULT.currentTest = null;
|
|
1232
|
-
if (
|
|
1233
|
-
|
|
1224
|
+
if (navigator.webdriver) {
|
|
1225
|
+
const isFailed = details.status === 'failed';
|
|
1226
|
+
const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
|
|
1227
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
|
|
1234
1228
|
|
|
1235
1229
|
if (${config.failFast} && details.status === 'failed') {
|
|
1236
1230
|
window.QUnit.config.queue.length = 0;
|
|
@@ -1238,8 +1232,8 @@ function testRuntimeToInject(port, config) {
|
|
|
1238
1232
|
}
|
|
1239
1233
|
});
|
|
1240
1234
|
window.QUnit.done((details) => {
|
|
1241
|
-
if (
|
|
1242
|
-
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1235
|
+
if (navigator.webdriver) {
|
|
1236
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1243
1237
|
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1244
1238
|
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
1245
1239
|
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
@@ -1329,7 +1323,7 @@ function buildNoTestsHTML(files) {
|
|
|
1329
1323
|
</head>
|
|
1330
1324
|
<body>
|
|
1331
1325
|
<div id="qunit">
|
|
1332
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1326
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1333
1327
|
<h2 id="qunit-banner"></h2>
|
|
1334
1328
|
<div id="qunit-userAgent">Warning: No Tests Registered</div>
|
|
1335
1329
|
<ol id="qunit-tests">
|
|
@@ -1349,7 +1343,7 @@ function buildNoTestsHTML(files) {
|
|
|
1349
1343
|
(function () {
|
|
1350
1344
|
var retries = 0;
|
|
1351
1345
|
function connect() {
|
|
1352
|
-
var ws = new WebSocket(
|
|
1346
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1353
1347
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1354
1348
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1355
1349
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1431,7 +1425,7 @@ function buildErrorHTML(buildError) {
|
|
|
1431
1425
|
</head>
|
|
1432
1426
|
<body>
|
|
1433
1427
|
<div id="qunit">
|
|
1434
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1428
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1435
1429
|
<h2 id="qunit-banner"></h2>
|
|
1436
1430
|
<div id="qunit-userAgent">Build Error: ${buildError.type}</div>
|
|
1437
1431
|
<ol id="qunit-tests">
|
|
@@ -1451,7 +1445,7 @@ function buildErrorHTML(buildError) {
|
|
|
1451
1445
|
(function () {
|
|
1452
1446
|
var retries = 0;
|
|
1453
1447
|
function connect() {
|
|
1454
|
-
var ws = new WebSocket(
|
|
1448
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1455
1449
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1456
1450
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1457
1451
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1463,16 +1457,42 @@ function buildErrorHTML(buildError) {
|
|
|
1463
1457
|
</body>
|
|
1464
1458
|
</html>`;
|
|
1465
1459
|
}
|
|
1466
|
-
var fsPromise;
|
|
1460
|
+
var fsPromise, NOT_FOUND_HTML;
|
|
1467
1461
|
var init_web_server = __esm({
|
|
1468
1462
|
"lib/setup/web-server.ts"() {
|
|
1469
1463
|
init_find_internal_assets_from_html();
|
|
1470
1464
|
init_html();
|
|
1471
1465
|
init_display_test_result();
|
|
1472
1466
|
init_color();
|
|
1473
|
-
init_path_exists();
|
|
1474
1467
|
init_http();
|
|
1475
1468
|
fsPromise = fs8.promises;
|
|
1469
|
+
NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
1470
|
+
<html lang="en">
|
|
1471
|
+
<head>
|
|
1472
|
+
<meta charset="utf-8">
|
|
1473
|
+
<meta name="viewport" content="width=device-width">
|
|
1474
|
+
<title>404 Not Found \u2014 qunitx</title>
|
|
1475
|
+
<style>
|
|
1476
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1477
|
+
body{font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial,sans-serif}
|
|
1478
|
+
#qunit-header{padding:.5em 0 .5em 1em;color:#C2CCD1;background-color:#0D3349;font-size:1.5em;line-height:1em;font-weight:400;border-radius:5px 5px 0 0}
|
|
1479
|
+
#qunit-banner{height:5px;background-color:#EE5757}
|
|
1480
|
+
#qunit-userAgent{padding:.5em 1em;color:#fff;background-color:#2B81AF;text-shadow:rgba(0,0,0,.5) 2px 2px 1px;font-size:small}
|
|
1481
|
+
#qunit-tests{list-style:none;font-size:smaller}
|
|
1482
|
+
#qunit-tests li{display:list-item;padding:.4em 1em;color:#000;background-color:#EE5757;border-radius:0 0 5px 5px}
|
|
1483
|
+
</style>
|
|
1484
|
+
</head>
|
|
1485
|
+
<body>
|
|
1486
|
+
<div id="qunit">
|
|
1487
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1488
|
+
<h2 id="qunit-banner"></h2>
|
|
1489
|
+
<div id="qunit-userAgent">404 Not Found</div>
|
|
1490
|
+
<ol id="qunit-tests">
|
|
1491
|
+
<li id="qunit-testresult"><script>document.getElementById('qunit-testresult').prepend(location.pathname)</script> was not found on this server.</li>
|
|
1492
|
+
</ol>
|
|
1493
|
+
</div>
|
|
1494
|
+
</body>
|
|
1495
|
+
</html>`;
|
|
1476
1496
|
}
|
|
1477
1497
|
});
|
|
1478
1498
|
|
|
@@ -1534,19 +1554,37 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1534
1554
|
const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
|
|
1535
1555
|
const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
|
|
1536
1556
|
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1557
|
+
if (config.browser === "firefox") {
|
|
1558
|
+
await page.addInitScript(() => {
|
|
1559
|
+
const preSerialize = (arg) => {
|
|
1560
|
+
if (arg === null || typeof arg !== "object") return arg;
|
|
1561
|
+
try {
|
|
1562
|
+
return JSON.stringify(arg, (_key, v) => v instanceof Date ? v.toISOString() : v);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return String(arg);
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
["log", "warn", "error", "info", "debug"].forEach((method) => {
|
|
1568
|
+
const orig = console[method].bind(console);
|
|
1569
|
+
console[method] = (...args) => orig(...args.map(preSerialize));
|
|
1570
|
+
});
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
|
|
1574
|
+
page.on("console", (msg) => {
|
|
1541
1575
|
const type = msg.type();
|
|
1542
1576
|
const alwaysShow = type === "warning" || type === "error";
|
|
1543
1577
|
if (!alwaysShow && !config.debug) return;
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1578
|
+
const handler = (async () => {
|
|
1579
|
+
try {
|
|
1580
|
+
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
|
|
1581
|
+
console.log(...values);
|
|
1582
|
+
} catch {
|
|
1583
|
+
console.log(msg.text());
|
|
1584
|
+
}
|
|
1585
|
+
})();
|
|
1586
|
+
config._pendingConsoleHandlers.add(handler);
|
|
1587
|
+
handler.finally(() => config._pendingConsoleHandlers?.delete(handler));
|
|
1550
1588
|
});
|
|
1551
1589
|
page.on("pageerror", (error) => {
|
|
1552
1590
|
console.error(error.toString());
|
|
@@ -1712,7 +1750,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1712
1750
|
// Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
|
|
1713
1751
|
// packages from any node_modules on the ancestor chain of cwd — the same lookup
|
|
1714
1752
|
// order Node itself uses when resolving require() from process.cwd().
|
|
1715
|
-
nodePaths:
|
|
1753
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
1716
1754
|
bundle: true,
|
|
1717
1755
|
logLevel: "silent",
|
|
1718
1756
|
outfile,
|
|
@@ -1817,6 +1855,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1817
1855
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
1818
1856
|
}
|
|
1819
1857
|
if (!config.watch) {
|
|
1858
|
+
await flushConsoleHandlers(config._pendingConsoleHandlers);
|
|
1820
1859
|
await Promise.all([
|
|
1821
1860
|
connections.server && connections.server.close(),
|
|
1822
1861
|
connections.browser && connections.browser.close()
|
|
@@ -1856,7 +1895,7 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1856
1895
|
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
1857
1896
|
resolveDir: process.cwd()
|
|
1858
1897
|
},
|
|
1859
|
-
nodePaths:
|
|
1898
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
1860
1899
|
bundle: true,
|
|
1861
1900
|
logLevel: "silent",
|
|
1862
1901
|
outfile: outputPath,
|
|
@@ -1958,7 +1997,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1958
1997
|
clearTimeout(timeoutHandle);
|
|
1959
1998
|
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1960
1999
|
await testRaceResult;
|
|
1961
|
-
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
2000
|
+
QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
|
|
1962
2001
|
} catch (error) {
|
|
1963
2002
|
targetError = error;
|
|
1964
2003
|
console.log(error);
|
|
@@ -1969,6 +2008,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1969
2008
|
config._onTestsJsServed = null;
|
|
1970
2009
|
config._resetTestTimeout = null;
|
|
1971
2010
|
config._testRunDone = null;
|
|
2011
|
+
config._lastQUnitResult = null;
|
|
1972
2012
|
}
|
|
1973
2013
|
if (!QUNIT_RESULT) {
|
|
1974
2014
|
if (targetError) console.log(targetError);
|
|
@@ -1976,7 +2016,12 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1976
2016
|
console.log(`# TIMEOUT: ${wsReason}`);
|
|
1977
2017
|
console.log("BROWSER: runtime error thrown during executing tests");
|
|
1978
2018
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
1979
|
-
await failOnNonWatchMode(
|
|
2019
|
+
await failOnNonWatchMode(
|
|
2020
|
+
config.watch,
|
|
2021
|
+
{ server, browser },
|
|
2022
|
+
config._groupMode,
|
|
2023
|
+
config._pendingConsoleHandlers
|
|
2024
|
+
);
|
|
1980
2025
|
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
1981
2026
|
return;
|
|
1982
2027
|
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
@@ -1986,16 +2031,22 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1986
2031
|
);
|
|
1987
2032
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1988
2033
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1989
|
-
await failOnNonWatchMode(
|
|
2034
|
+
await failOnNonWatchMode(
|
|
2035
|
+
config.watch,
|
|
2036
|
+
{ server, browser },
|
|
2037
|
+
config._groupMode,
|
|
2038
|
+
config._pendingConsoleHandlers
|
|
2039
|
+
);
|
|
1990
2040
|
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
1991
2041
|
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
1992
2042
|
}
|
|
1993
2043
|
}
|
|
1994
|
-
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
|
|
2044
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
1995
2045
|
if (!watchMode) {
|
|
1996
2046
|
if (groupMode) {
|
|
1997
2047
|
throw new Error("Browser test run failed");
|
|
1998
2048
|
}
|
|
2049
|
+
await flushConsoleHandlers(pendingHandlers);
|
|
1999
2050
|
await Promise.all([
|
|
2000
2051
|
connections.server && connections.server.close(),
|
|
2001
2052
|
connections.browser && connections.browser.close()
|
|
@@ -2004,7 +2055,12 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
2004
2055
|
process.exit(1);
|
|
2005
2056
|
}
|
|
2006
2057
|
}
|
|
2007
|
-
|
|
2058
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + 2e3) {
|
|
2059
|
+
if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
|
|
2060
|
+
await Promise.allSettled([...handlers]);
|
|
2061
|
+
return flushConsoleHandlers(handlers, deadline);
|
|
2062
|
+
}
|
|
2063
|
+
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES;
|
|
2008
2064
|
var init_tests_in_browser = __esm({
|
|
2009
2065
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2010
2066
|
init_color();
|
|
@@ -2023,6 +2079,7 @@ var init_tests_in_browser = __esm({
|
|
|
2023
2079
|
ancestorNodeModules = (dir) => dir.split(path5.sep).map(
|
|
2024
2080
|
(_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
|
|
2025
2081
|
);
|
|
2082
|
+
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
2026
2083
|
}
|
|
2027
2084
|
});
|
|
2028
2085
|
|
|
@@ -2209,7 +2266,7 @@ var CHANGE_DEDUPE_MS;
|
|
|
2209
2266
|
var init_file_watcher = __esm({
|
|
2210
2267
|
"lib/setup/file-watcher.ts"() {
|
|
2211
2268
|
init_color();
|
|
2212
|
-
CHANGE_DEDUPE_MS =
|
|
2269
|
+
CHANGE_DEDUPE_MS = 10;
|
|
2213
2270
|
}
|
|
2214
2271
|
});
|
|
2215
2272
|
|
|
@@ -2317,7 +2374,9 @@ var init_write_output_static_files = __esm({
|
|
|
2317
2374
|
// lib/commands/run.ts
|
|
2318
2375
|
var run_exports = {};
|
|
2319
2376
|
__export(run_exports, {
|
|
2377
|
+
computeFileTimes: () => computeFileTimes,
|
|
2320
2378
|
default: () => run,
|
|
2379
|
+
readTimingCache: () => readTimingCache,
|
|
2321
2380
|
run: () => run
|
|
2322
2381
|
});
|
|
2323
2382
|
import fs12 from "node:fs/promises";
|
|
@@ -2393,7 +2452,8 @@ async function run(config) {
|
|
|
2393
2452
|
} else {
|
|
2394
2453
|
const allFiles = Object.keys(config.fsTree);
|
|
2395
2454
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2396
|
-
const
|
|
2455
|
+
const timings = await readTimingCache(config.projectRoot);
|
|
2456
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2397
2457
|
config.COUNTER = {
|
|
2398
2458
|
testCount: 0,
|
|
2399
2459
|
failCount: 0,
|
|
@@ -2432,6 +2492,7 @@ async function run(config) {
|
|
|
2432
2492
|
void openOutputInBrowser(config);
|
|
2433
2493
|
}
|
|
2434
2494
|
const TIME_COUNTER = timeCounter();
|
|
2495
|
+
const wallTimes = /* @__PURE__ */ new Map();
|
|
2435
2496
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2436
2497
|
const keepAlive = setInterval(() => {
|
|
2437
2498
|
}, 1e3);
|
|
@@ -2451,34 +2512,36 @@ async function run(config) {
|
|
|
2451
2512
|
}, GROUP_TIMEOUT_MS);
|
|
2452
2513
|
timeoutId.unref();
|
|
2453
2514
|
});
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
]).catch(() => {
|
|
2515
|
+
const startMs = Date.now();
|
|
2516
|
+
const work = (async () => {
|
|
2517
|
+
groupConfig._phase = "connecting";
|
|
2518
|
+
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
2519
|
+
groupConfig.expressApp = connections.server;
|
|
2520
|
+
if (config.before) {
|
|
2521
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
2522
|
+
}
|
|
2523
|
+
try {
|
|
2524
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
2525
|
+
} finally {
|
|
2526
|
+
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
2527
|
+
await Promise.all([
|
|
2528
|
+
connections.server && connections.server.close(),
|
|
2529
|
+
connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
2530
|
+
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
2531
|
+
Promise.race([
|
|
2532
|
+
connections.page.close(),
|
|
2533
|
+
new Promise((resolve) => {
|
|
2534
|
+
const pageCloseTimeoutId = setTimeout(resolve, 1e4);
|
|
2535
|
+
pageCloseTimeoutId.unref();
|
|
2476
2536
|
})
|
|
2477
|
-
])
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2537
|
+
]).catch(() => {
|
|
2538
|
+
})
|
|
2539
|
+
]);
|
|
2540
|
+
}
|
|
2541
|
+
})();
|
|
2542
|
+
const record = () => wallTimes.set(i, Date.now() - startMs);
|
|
2543
|
+
work.then(record, record);
|
|
2544
|
+
return Promise.race([work, groupTimeout]);
|
|
2482
2545
|
})
|
|
2483
2546
|
);
|
|
2484
2547
|
const exitCode = groupResults.reduce(
|
|
@@ -2497,6 +2560,10 @@ async function run(config) {
|
|
|
2497
2560
|
);
|
|
2498
2561
|
}
|
|
2499
2562
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
2563
|
+
const fileTimes = computeFileTimes(groups, weights, wallTimes);
|
|
2564
|
+
persistTimings(fileTimes, config.projectRoot).catch(() => {
|
|
2565
|
+
});
|
|
2566
|
+
printFileTimings(fileTimes, config.projectRoot);
|
|
2500
2567
|
if (config.after) {
|
|
2501
2568
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2502
2569
|
}
|
|
@@ -2567,10 +2634,57 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
2567
2634
|
}
|
|
2568
2635
|
return cachedContent;
|
|
2569
2636
|
}
|
|
2570
|
-
function
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2637
|
+
async function readTimingCache(projectRoot) {
|
|
2638
|
+
try {
|
|
2639
|
+
const parsed = JSON.parse(await fs12.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
2640
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
2641
|
+
} catch {
|
|
2642
|
+
return {};
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function computeFileTimes(groups, weights, wallTimes) {
|
|
2646
|
+
const result = /* @__PURE__ */ new Map();
|
|
2647
|
+
groups.forEach((group, i) => {
|
|
2648
|
+
const wallMs = wallTimes.get(i);
|
|
2649
|
+
if (wallMs === void 0) return;
|
|
2650
|
+
const total = group.reduce((sum, f) => sum + (weights.get(f) ?? 0), 0);
|
|
2651
|
+
group.forEach(
|
|
2652
|
+
(f) => result.set(f, total > 0 ? wallMs * ((weights.get(f) ?? 0) / total) : wallMs / group.length)
|
|
2653
|
+
);
|
|
2654
|
+
});
|
|
2655
|
+
return result;
|
|
2656
|
+
}
|
|
2657
|
+
async function persistTimings(fileTimes, projectRoot) {
|
|
2658
|
+
await fs12.writeFile(
|
|
2659
|
+
`${projectRoot}/tmp/test-timings.json`,
|
|
2660
|
+
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
function printFileTimings(fileTimes, projectRoot) {
|
|
2664
|
+
if (fileTimes.size === 0) return;
|
|
2665
|
+
const lines = [...fileTimes.entries()].sort(([, a], [, b]) => b - a).map(([f, ms]) => `# ${ms.toFixed(0)}ms ${f.replace(`${projectRoot}/`, "")}`);
|
|
2666
|
+
process.stdout.write(`# File execution times:
|
|
2667
|
+
${lines.join("\n")}
|
|
2668
|
+
`);
|
|
2669
|
+
}
|
|
2670
|
+
async function splitIntoGroups(files, groupCount, timings) {
|
|
2671
|
+
const sizes = await Promise.all(
|
|
2672
|
+
files.map(
|
|
2673
|
+
(f) => fs12.stat(f).then((s) => s.size).catch(() => 0)
|
|
2674
|
+
)
|
|
2675
|
+
);
|
|
2676
|
+
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
2677
|
+
const msPerByte = knownRates.length > 0 ? knownRates.reduce((sum, { ms, size }) => sum + ms / size, 0) / knownRates.length : 1;
|
|
2678
|
+
const weights = new Map(
|
|
2679
|
+
files.map((f, i) => [f, timings[f] > 0 ? timings[f] : sizes[i] * msPerByte])
|
|
2680
|
+
);
|
|
2681
|
+
const buckets = Array.from({ length: groupCount }, () => ({ files: [], total: 0 }));
|
|
2682
|
+
[...files].sort((a, b) => (weights.get(b) ?? 0) - (weights.get(a) ?? 0)).forEach((f) => {
|
|
2683
|
+
const min = buckets.reduce((m, _, i) => buckets[i].total < buckets[m].total ? i : m, 0);
|
|
2684
|
+
buckets[min].files.push(f);
|
|
2685
|
+
buckets[min].total += weights.get(f) ?? 0;
|
|
2686
|
+
});
|
|
2687
|
+
return { groups: buckets.filter((b) => b.files.length > 0).map((b) => b.files), weights };
|
|
2574
2688
|
}
|
|
2575
2689
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
2576
2690
|
const prefix = "Watching files...";
|
|
@@ -2619,7 +2733,7 @@ init_color();
|
|
|
2619
2733
|
var package_default = {
|
|
2620
2734
|
name: "qunitx-cli",
|
|
2621
2735
|
type: "module",
|
|
2622
|
-
version: "0.19.
|
|
2736
|
+
version: "0.19.2",
|
|
2623
2737
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2624
2738
|
author: "Izel Nakri",
|
|
2625
2739
|
license: "MIT",
|
|
@@ -2735,8 +2849,18 @@ import path2 from "node:path";
|
|
|
2735
2849
|
// lib/utils/find-project-root.ts
|
|
2736
2850
|
import process2 from "node:process";
|
|
2737
2851
|
|
|
2852
|
+
// lib/utils/path-exists.ts
|
|
2853
|
+
import fs2 from "node:fs/promises";
|
|
2854
|
+
async function pathExists(path7) {
|
|
2855
|
+
try {
|
|
2856
|
+
await fs2.access(path7);
|
|
2857
|
+
return true;
|
|
2858
|
+
} catch {
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2738
2863
|
// lib/utils/search-in-parent-directories.ts
|
|
2739
|
-
init_path_exists();
|
|
2740
2864
|
async function searchInParentDirectories(directory, targetEntry) {
|
|
2741
2865
|
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
2742
2866
|
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
@@ -2764,9 +2888,6 @@ async function findProjectRoot() {
|
|
|
2764
2888
|
}
|
|
2765
2889
|
}
|
|
2766
2890
|
|
|
2767
|
-
// lib/commands/init.ts
|
|
2768
|
-
init_path_exists();
|
|
2769
|
-
|
|
2770
2891
|
// lib/setup/default-project-config-values.ts
|
|
2771
2892
|
var defaultProjectConfigValues = {
|
|
2772
2893
|
output: "tmp",
|
|
@@ -2833,7 +2954,6 @@ async function writeTSConfigIfNeeded(projectRoot) {
|
|
|
2833
2954
|
// lib/commands/generate.ts
|
|
2834
2955
|
init_color();
|
|
2835
2956
|
import fs5 from "node:fs/promises";
|
|
2836
|
-
init_path_exists();
|
|
2837
2957
|
init_read_template();
|
|
2838
2958
|
|
|
2839
2959
|
// lib/utils/convert-to-pascal-case.ts
|