qunitx-cli 0.19.1 → 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 +182 -106
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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,6 +891,7 @@ 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
897
|
const { event, details, qunitResult, abort } = JSON.parse(data);
|
|
@@ -957,7 +943,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
957
943
|
});
|
|
958
944
|
server.get("/tests.js", (_req, res) => {
|
|
959
945
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
960
|
-
process.stdout.write(
|
|
946
|
+
config.debug && process.stdout.write(
|
|
961
947
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
962
948
|
`
|
|
963
949
|
);
|
|
@@ -978,7 +964,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
978
964
|
});
|
|
979
965
|
server.get("/filtered-tests.js", (_req, res) => {
|
|
980
966
|
const bytes = cachedContent.filteredTestCode?.length ?? null;
|
|
981
|
-
process.stdout.write(
|
|
967
|
+
config.debug && process.stdout.write(
|
|
982
968
|
`# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
|
|
983
969
|
`
|
|
984
970
|
);
|
|
@@ -1017,7 +1003,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1017
1003
|
}
|
|
1018
1004
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1019
1005
|
mainHTMLWithReplacedAssets,
|
|
1020
|
-
|
|
1006
|
+
runtimeScript,
|
|
1021
1007
|
"./tests.js"
|
|
1022
1008
|
);
|
|
1023
1009
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1048,7 +1034,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1048
1034
|
}
|
|
1049
1035
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1050
1036
|
mainHTMLWithReplacedAssets,
|
|
1051
|
-
|
|
1037
|
+
runtimeScript,
|
|
1052
1038
|
"./filtered-tests.js"
|
|
1053
1039
|
);
|
|
1054
1040
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1064,7 +1050,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1064
1050
|
if (possibleDynamicHTML) {
|
|
1065
1051
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1066
1052
|
possibleDynamicHTML,
|
|
1067
|
-
|
|
1053
|
+
runtimeScript,
|
|
1068
1054
|
"/tests.js"
|
|
1069
1055
|
);
|
|
1070
1056
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
@@ -1076,21 +1062,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
1076
1062
|
);
|
|
1077
1063
|
}
|
|
1078
1064
|
const url = req.url;
|
|
1079
|
-
const requestStartedAt =
|
|
1065
|
+
const requestStartedAt = Date.now();
|
|
1080
1066
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
+
);
|
|
1084
1076
|
});
|
|
1085
|
-
|
|
1086
|
-
res.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
process.stdout.write(
|
|
1091
|
-
`# [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
|
|
1092
1082
|
`
|
|
1093
|
-
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1094
1085
|
});
|
|
1095
1086
|
return server;
|
|
1096
1087
|
}
|
|
@@ -1102,7 +1093,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1102
1093
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1103
1094
|
}, html);
|
|
1104
1095
|
}
|
|
1105
|
-
function testRuntimeToInject(
|
|
1096
|
+
function testRuntimeToInject(config) {
|
|
1106
1097
|
return `<script>
|
|
1107
1098
|
window.testTimeout = 0;
|
|
1108
1099
|
setInterval(() => {
|
|
@@ -1138,7 +1129,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1138
1129
|
|
|
1139
1130
|
function setupWebSocket() {
|
|
1140
1131
|
try {
|
|
1141
|
-
window.socket = new WebSocket(
|
|
1132
|
+
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1142
1133
|
} catch (error) {
|
|
1143
1134
|
console.log(error);
|
|
1144
1135
|
retryOrFail();
|
|
@@ -1150,7 +1141,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1150
1141
|
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1151
1142
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1152
1143
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1153
|
-
if (
|
|
1144
|
+
if (navigator.webdriver) {
|
|
1154
1145
|
window.socket.send(JSON.stringify({ event: 'wsOpen' }));
|
|
1155
1146
|
}
|
|
1156
1147
|
maybeStart();
|
|
@@ -1159,9 +1150,9 @@ function testRuntimeToInject(port, config) {
|
|
|
1159
1150
|
retryOrFail();
|
|
1160
1151
|
});
|
|
1161
1152
|
window.socket.addEventListener('message', function(messageEvent) {
|
|
1162
|
-
if (!
|
|
1153
|
+
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1163
1154
|
window.location.reload(true);
|
|
1164
|
-
} else if (
|
|
1155
|
+
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1165
1156
|
window.abortQUnit = true;
|
|
1166
1157
|
window.QUnit.config.queue.length = 0;
|
|
1167
1158
|
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
@@ -1204,7 +1195,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1204
1195
|
|
|
1205
1196
|
if (!window.QUnit) {
|
|
1206
1197
|
console.log('QUnit not found after WebSocket connected');
|
|
1207
|
-
if (
|
|
1198
|
+
if (navigator.webdriver) {
|
|
1208
1199
|
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1209
1200
|
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1210
1201
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
@@ -1217,7 +1208,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1217
1208
|
}
|
|
1218
1209
|
|
|
1219
1210
|
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1220
|
-
if (
|
|
1211
|
+
if (navigator.webdriver) {
|
|
1221
1212
|
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1222
1213
|
}
|
|
1223
1214
|
});
|
|
@@ -1230,8 +1221,10 @@ function testRuntimeToInject(port, config) {
|
|
|
1230
1221
|
window.QUNIT_RESULT.finishedTests++;
|
|
1231
1222
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1232
1223
|
window.QUNIT_RESULT.currentTest = null;
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
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));
|
|
1235
1228
|
|
|
1236
1229
|
if (${config.failFast} && details.status === 'failed') {
|
|
1237
1230
|
window.QUnit.config.queue.length = 0;
|
|
@@ -1239,7 +1232,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1239
1232
|
}
|
|
1240
1233
|
});
|
|
1241
1234
|
window.QUnit.done((details) => {
|
|
1242
|
-
if (
|
|
1235
|
+
if (navigator.webdriver) {
|
|
1243
1236
|
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1244
1237
|
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1245
1238
|
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
@@ -1330,7 +1323,7 @@ function buildNoTestsHTML(files) {
|
|
|
1330
1323
|
</head>
|
|
1331
1324
|
<body>
|
|
1332
1325
|
<div id="qunit">
|
|
1333
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1326
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1334
1327
|
<h2 id="qunit-banner"></h2>
|
|
1335
1328
|
<div id="qunit-userAgent">Warning: No Tests Registered</div>
|
|
1336
1329
|
<ol id="qunit-tests">
|
|
@@ -1350,7 +1343,7 @@ function buildNoTestsHTML(files) {
|
|
|
1350
1343
|
(function () {
|
|
1351
1344
|
var retries = 0;
|
|
1352
1345
|
function connect() {
|
|
1353
|
-
var ws = new WebSocket(
|
|
1346
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1354
1347
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1355
1348
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1356
1349
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1432,7 +1425,7 @@ function buildErrorHTML(buildError) {
|
|
|
1432
1425
|
</head>
|
|
1433
1426
|
<body>
|
|
1434
1427
|
<div id="qunit">
|
|
1435
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1428
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1436
1429
|
<h2 id="qunit-banner"></h2>
|
|
1437
1430
|
<div id="qunit-userAgent">Build Error: ${buildError.type}</div>
|
|
1438
1431
|
<ol id="qunit-tests">
|
|
@@ -1452,7 +1445,7 @@ function buildErrorHTML(buildError) {
|
|
|
1452
1445
|
(function () {
|
|
1453
1446
|
var retries = 0;
|
|
1454
1447
|
function connect() {
|
|
1455
|
-
var ws = new WebSocket(
|
|
1448
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1456
1449
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1457
1450
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1458
1451
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1464,16 +1457,42 @@ function buildErrorHTML(buildError) {
|
|
|
1464
1457
|
</body>
|
|
1465
1458
|
</html>`;
|
|
1466
1459
|
}
|
|
1467
|
-
var fsPromise;
|
|
1460
|
+
var fsPromise, NOT_FOUND_HTML;
|
|
1468
1461
|
var init_web_server = __esm({
|
|
1469
1462
|
"lib/setup/web-server.ts"() {
|
|
1470
1463
|
init_find_internal_assets_from_html();
|
|
1471
1464
|
init_html();
|
|
1472
1465
|
init_display_test_result();
|
|
1473
1466
|
init_color();
|
|
1474
|
-
init_path_exists();
|
|
1475
1467
|
init_http();
|
|
1476
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>`;
|
|
1477
1496
|
}
|
|
1478
1497
|
});
|
|
1479
1498
|
|
|
@@ -1535,9 +1554,22 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1535
1554
|
const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
|
|
1536
1555
|
const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
|
|
1537
1556
|
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
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
|
+
}
|
|
1541
1573
|
config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
|
|
1542
1574
|
page.on("console", (msg) => {
|
|
1543
1575
|
const type = msg.type();
|
|
@@ -1545,11 +1577,7 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1545
1577
|
if (!alwaysShow && !config.debug) return;
|
|
1546
1578
|
const handler = (async () => {
|
|
1547
1579
|
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
|
-
);
|
|
1580
|
+
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
|
|
1553
1581
|
console.log(...values);
|
|
1554
1582
|
} catch {
|
|
1555
1583
|
console.log(msg.text());
|
|
@@ -2346,7 +2374,9 @@ var init_write_output_static_files = __esm({
|
|
|
2346
2374
|
// lib/commands/run.ts
|
|
2347
2375
|
var run_exports = {};
|
|
2348
2376
|
__export(run_exports, {
|
|
2377
|
+
computeFileTimes: () => computeFileTimes,
|
|
2349
2378
|
default: () => run,
|
|
2379
|
+
readTimingCache: () => readTimingCache,
|
|
2350
2380
|
run: () => run
|
|
2351
2381
|
});
|
|
2352
2382
|
import fs12 from "node:fs/promises";
|
|
@@ -2422,7 +2452,8 @@ async function run(config) {
|
|
|
2422
2452
|
} else {
|
|
2423
2453
|
const allFiles = Object.keys(config.fsTree);
|
|
2424
2454
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2425
|
-
const
|
|
2455
|
+
const timings = await readTimingCache(config.projectRoot);
|
|
2456
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2426
2457
|
config.COUNTER = {
|
|
2427
2458
|
testCount: 0,
|
|
2428
2459
|
failCount: 0,
|
|
@@ -2461,6 +2492,7 @@ async function run(config) {
|
|
|
2461
2492
|
void openOutputInBrowser(config);
|
|
2462
2493
|
}
|
|
2463
2494
|
const TIME_COUNTER = timeCounter();
|
|
2495
|
+
const wallTimes = /* @__PURE__ */ new Map();
|
|
2464
2496
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2465
2497
|
const keepAlive = setInterval(() => {
|
|
2466
2498
|
}, 1e3);
|
|
@@ -2480,35 +2512,36 @@ async function run(config) {
|
|
|
2480
2512
|
}, GROUP_TIMEOUT_MS);
|
|
2481
2513
|
timeoutId.unref();
|
|
2482
2514
|
});
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
})
|
|
2505
|
-
]).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();
|
|
2506
2536
|
})
|
|
2507
|
-
])
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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]);
|
|
2512
2545
|
})
|
|
2513
2546
|
);
|
|
2514
2547
|
const exitCode = groupResults.reduce(
|
|
@@ -2527,6 +2560,10 @@ async function run(config) {
|
|
|
2527
2560
|
);
|
|
2528
2561
|
}
|
|
2529
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);
|
|
2530
2567
|
if (config.after) {
|
|
2531
2568
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2532
2569
|
}
|
|
@@ -2597,24 +2634,57 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
2597
2634
|
}
|
|
2598
2635
|
return cachedContent;
|
|
2599
2636
|
}
|
|
2600
|
-
async function
|
|
2601
|
-
|
|
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(
|
|
2602
2672
|
files.map(
|
|
2603
|
-
(f) => fs12.stat(f).then((
|
|
2673
|
+
(f) => fs12.stat(f).then((s) => s.size).catch(() => 0)
|
|
2604
2674
|
)
|
|
2605
2675
|
);
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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 };
|
|
2618
2688
|
}
|
|
2619
2689
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
2620
2690
|
const prefix = "Watching files...";
|
|
@@ -2663,7 +2733,7 @@ init_color();
|
|
|
2663
2733
|
var package_default = {
|
|
2664
2734
|
name: "qunitx-cli",
|
|
2665
2735
|
type: "module",
|
|
2666
|
-
version: "0.19.
|
|
2736
|
+
version: "0.19.2",
|
|
2667
2737
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2668
2738
|
author: "Izel Nakri",
|
|
2669
2739
|
license: "MIT",
|
|
@@ -2779,8 +2849,18 @@ import path2 from "node:path";
|
|
|
2779
2849
|
// lib/utils/find-project-root.ts
|
|
2780
2850
|
import process2 from "node:process";
|
|
2781
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
|
+
|
|
2782
2863
|
// lib/utils/search-in-parent-directories.ts
|
|
2783
|
-
init_path_exists();
|
|
2784
2864
|
async function searchInParentDirectories(directory, targetEntry) {
|
|
2785
2865
|
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
2786
2866
|
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
@@ -2808,9 +2888,6 @@ async function findProjectRoot() {
|
|
|
2808
2888
|
}
|
|
2809
2889
|
}
|
|
2810
2890
|
|
|
2811
|
-
// lib/commands/init.ts
|
|
2812
|
-
init_path_exists();
|
|
2813
|
-
|
|
2814
2891
|
// lib/setup/default-project-config-values.ts
|
|
2815
2892
|
var defaultProjectConfigValues = {
|
|
2816
2893
|
output: "tmp",
|
|
@@ -2877,7 +2954,6 @@ async function writeTSConfigIfNeeded(projectRoot) {
|
|
|
2877
2954
|
// lib/commands/generate.ts
|
|
2878
2955
|
init_color();
|
|
2879
2956
|
import fs5 from "node:fs/promises";
|
|
2880
|
-
init_path_exists();
|
|
2881
2957
|
init_read_template();
|
|
2882
2958
|
|
|
2883
2959
|
// lib/utils/convert-to-pascal-case.ts
|