qunitx-cli 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +453 -81
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -387,9 +387,9 @@ var init_color = __esm({
387
387
 
388
388
  // lib/utils/path-exists.ts
389
389
  import fs2 from "node:fs/promises";
390
- async function pathExists(path6) {
390
+ async function pathExists(path7) {
391
391
  try {
392
- await fs2.access(path6);
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
- process.stdout.write(`not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # skip
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(path6, handler) {
747
- this.#registerRouteHandler("GET", path6, handler);
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(path6, handler) {
779
- this.#registerRouteHandler("POST", path6, handler);
779
+ post(path7, handler) {
780
+ this.#registerRouteHandler("POST", path7, handler);
780
781
  }
781
782
  /** Registers a DELETE route handler. */
782
- delete(path6, handler) {
783
- this.#registerRouteHandler("DELETE", path6, handler);
783
+ delete(path7, handler) {
784
+ this.#registerRouteHandler("DELETE", path7, handler);
784
785
  }
785
786
  /** Registers a PUT route handler. */
786
- put(path6, handler) {
787
- this.#registerRouteHandler("PUT", path6, handler);
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, path6, handler) {
794
+ #registerRouteHandler(method, path7, handler) {
794
795
  if (!this.routes[method]) {
795
796
  this.routes[method] = {};
796
797
  }
797
- this.routes[method][path6] = {
798
- path: path6,
798
+ this.routes[method][path7] = {
799
+ path: path7,
799
800
  handler,
800
- paramNames: this.#extractParamNames(path6),
801
- isWildcard: path6 === "/*"
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: path6, isWildcard } = route;
840
- if (!isWildcard && !path6.includes(":")) {
840
+ const { path: path7, isWildcard } = route;
841
+ if (!isWildcard && !path7.includes(":")) {
841
842
  return false;
842
843
  }
843
- if (isWildcard || this.#matchPathSegments(path6, url)) {
844
+ if (isWildcard || this.#matchPathSegments(path7, url)) {
844
845
  if (route.paramNames.length > 0) {
845
- const regexPattern = this.#buildRegexPattern(path6, route.paramNames);
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(path6, url) {
858
- const pathSegments = path6.split("/");
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(path6, _paramNames) {
876
- let regexPattern = path6.replace(/:[^/]+/g, "([^/]+)");
876
+ #buildRegexPattern(path7, _paramNames) {
877
+ let regexPattern = path7.replace(/:[^/]+/g, "([^/]+)");
877
878
  regexPattern = regexPattern.replace(/\//g, "\\/");
878
879
  return regexPattern;
879
880
  }
880
- #extractParamNames(path6) {
881
+ #extractParamNames(path7) {
881
882
  const paramRegex = /:(\w+)/g;
882
- const paramMatches = path6.match(paramRegex);
883
+ const paramMatches = path7.match(paramRegex);
883
884
  return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
884
885
  }
885
886
  #extractParams(route, _url) {
@@ -988,6 +989,23 @@ function setupWebServer(config, cachedContent) {
988
989
  res.end(cachedContent.filteredTestCode);
989
990
  });
990
991
  server.get("/", async (_req, res) => {
992
+ if (cachedContent._buildError) {
993
+ const htmlContent2 = buildErrorHTML(cachedContent._buildError);
994
+ res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
995
+ res.write(htmlContent2);
996
+ res.end();
997
+ return await fsPromise.writeFile(
998
+ `${config.projectRoot}/${config.output}/index.html`,
999
+ htmlContent2
1000
+ );
1001
+ }
1002
+ if (cachedContent._noTestsWarning) {
1003
+ const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
1004
+ res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
1005
+ res.write(htmlContent2);
1006
+ res.end();
1007
+ return;
1008
+ }
991
1009
  const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
992
1010
  const htmlContent = escapeAndInjectTestsToHTML(
993
1011
  mainHTMLWithReplacedAssets,
@@ -1003,6 +1021,23 @@ function setupWebServer(config, cachedContent) {
1003
1021
  );
1004
1022
  });
1005
1023
  server.get("/qunitx.html", async (_req, res) => {
1024
+ if (cachedContent._buildError) {
1025
+ const htmlContent2 = buildErrorHTML(cachedContent._buildError);
1026
+ res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
1027
+ res.write(htmlContent2);
1028
+ res.end();
1029
+ return await fsPromise.writeFile(
1030
+ `${config.projectRoot}/${config.output}/qunitx.html`,
1031
+ htmlContent2
1032
+ );
1033
+ }
1034
+ if (cachedContent._noTestsWarning) {
1035
+ const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
1036
+ res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
1037
+ res.write(htmlContent2);
1038
+ res.end();
1039
+ return;
1040
+ }
1006
1041
  const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
1007
1042
  const htmlContent = escapeAndInjectTestsToHTML(
1008
1043
  mainHTMLWithReplacedAssets,
@@ -1163,7 +1198,15 @@ function testRuntimeToInject(port, config) {
1163
1198
 
1164
1199
  if (!window.QUnit) {
1165
1200
  console.log('QUnit not found after WebSocket connected');
1166
- window.testTimeout = ${config.timeout};
1201
+ if (window.IS_PLAYWRIGHT) {
1202
+ // Signal the Playwright runner that the run is complete with 0 tests rather than
1203
+ // waiting for the inactivity timeout. The runner treats totalTests === 0 as a
1204
+ // "no tests registered" warning (not a failure), so this gives a fast, clean result.
1205
+ window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
1206
+ window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
1207
+ } else {
1208
+ window.testTimeout = ${config.timeout};
1209
+ }
1167
1210
  return;
1168
1211
  }
1169
1212
 
@@ -1215,6 +1258,211 @@ function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
1215
1258
  return injectScript(html, `${testRuntimeCode}
1216
1259
  <script src="${testBundleUrl}" async></script>`);
1217
1260
  }
1261
+ function buildNoTestsHTML(files) {
1262
+ const escaped = files.map((f) => f.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n");
1263
+ return `<!DOCTYPE html>
1264
+ <html lang="en">
1265
+ <head>
1266
+ <meta charset="utf-8">
1267
+ <meta name="viewport" content="width=device-width">
1268
+ <title>No Tests Registered \u2014 qunitx</title>
1269
+ <style>
1270
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1271
+ #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
1272
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
1273
+ }
1274
+ #qunit-header {
1275
+ padding: 0.5em 0 0.5em 1em;
1276
+ color: #C2CCD1;
1277
+ background-color: #0D3349;
1278
+ font-size: 1.5em;
1279
+ line-height: 1em;
1280
+ font-weight: 400;
1281
+ border-radius: 5px 5px 0 0;
1282
+ }
1283
+ #qunit-banner { height: 5px; background-color: #F0AD4E; }
1284
+ #qunit-userAgent {
1285
+ padding: 0.5em 1em;
1286
+ color: #fff;
1287
+ background-color: #EC971F;
1288
+ text-shadow: rgba(0,0,0,.3) 2px 2px 1px;
1289
+ font-size: small;
1290
+ }
1291
+ #qunit-tests { list-style: none; font-size: smaller; }
1292
+ #qunit-tests li.warn {
1293
+ display: list-item;
1294
+ padding: 0.4em 1em;
1295
+ border-bottom: 1px solid #fff;
1296
+ color: #000;
1297
+ background-color: #FCF8E3;
1298
+ border-left: 5px solid #F0AD4E;
1299
+ }
1300
+ #qunit-tests li.warn:last-child { border-radius: 0 0 5px 5px; }
1301
+ .qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
1302
+ .qunit-assert-list > li {
1303
+ padding: 5px;
1304
+ background-color: #FFF8DC;
1305
+ border-left: 10px solid #F0AD4E;
1306
+ color: #8A6D3B;
1307
+ }
1308
+ .qunit-assert-list pre {
1309
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
1310
+ font-size: 12px;
1311
+ line-height: 1.6;
1312
+ white-space: pre-wrap;
1313
+ word-break: break-word;
1314
+ color: #8A6D3B;
1315
+ margin: 0;
1316
+ }
1317
+ #qunit-testresult {
1318
+ padding: 0.5em 1em;
1319
+ color: #366097;
1320
+ background-color: #E2F0F7;
1321
+ border-bottom: 1px solid #fff;
1322
+ font-size: small;
1323
+ }
1324
+ .dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
1325
+ .dots span:nth-child(2) { animation-delay: .2s; }
1326
+ .dots span:nth-child(3) { animation-delay: .4s; }
1327
+ @keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
1328
+ </style>
1329
+ </head>
1330
+ <body>
1331
+ <div id="qunit">
1332
+ <h1 id="qunit-header">qunitx</h1>
1333
+ <h2 id="qunit-banner"></h2>
1334
+ <div id="qunit-userAgent">Warning: No Tests Registered</div>
1335
+ <ol id="qunit-tests">
1336
+ <li class="warn">
1337
+ <strong>0 QUnit tests were registered in the bundled file(s)</strong>
1338
+ <ol class="qunit-assert-list">
1339
+ <li><pre>${escaped}</pre></li>
1340
+ </ol>
1341
+ </li>
1342
+ </ol>
1343
+ <div id="qunit-testresult">
1344
+ Watching for changes&nbsp;<span class="dots"><span>&#9679;</span><span>&#9679;</span><span>&#9679;</span></span>
1345
+ </div>
1346
+ </div>
1347
+ <script>
1348
+ if (location.port) {
1349
+ (function () {
1350
+ var retries = 0;
1351
+ function connect() {
1352
+ var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
1353
+ ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1354
+ ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1355
+ ws.addEventListener('error', function () { ws.close(); });
1356
+ }
1357
+ connect();
1358
+ })();
1359
+ }
1360
+ </script>
1361
+ </body>
1362
+ </html>`;
1363
+ }
1364
+ function buildErrorHTML(buildError) {
1365
+ const escaped = buildError.formatted.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1366
+ return `<!DOCTYPE html>
1367
+ <html lang="en">
1368
+ <head>
1369
+ <meta charset="utf-8">
1370
+ <meta name="viewport" content="width=device-width">
1371
+ <title>Build Error \u2014 qunitx</title>
1372
+ <style>
1373
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1374
+ #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
1375
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
1376
+ }
1377
+ #qunit-header {
1378
+ padding: 0.5em 0 0.5em 1em;
1379
+ color: #C2CCD1;
1380
+ background-color: #0D3349;
1381
+ font-size: 1.5em;
1382
+ line-height: 1em;
1383
+ font-weight: 400;
1384
+ border-radius: 5px 5px 0 0;
1385
+ }
1386
+ #qunit-banner { height: 5px; background-color: #EE5757; }
1387
+ #qunit-userAgent {
1388
+ padding: 0.5em 1em;
1389
+ color: #fff;
1390
+ background-color: #2B81AF;
1391
+ text-shadow: rgba(0,0,0,.5) 2px 2px 1px;
1392
+ font-size: small;
1393
+ }
1394
+ #qunit-tests { list-style: none; font-size: smaller; }
1395
+ #qunit-tests li.fail {
1396
+ display: list-item;
1397
+ padding: 0.4em 1em;
1398
+ border-bottom: 1px solid #fff;
1399
+ color: #000;
1400
+ background-color: #EE5757;
1401
+ }
1402
+ #qunit-tests li.fail:last-child { border-radius: 0 0 5px 5px; }
1403
+ .qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
1404
+ .qunit-assert-list > li {
1405
+ padding: 5px;
1406
+ background-color: #fff;
1407
+ border-left: 10px solid #EE5757;
1408
+ color: #710909;
1409
+ }
1410
+ .qunit-assert-list pre {
1411
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
1412
+ font-size: 12px;
1413
+ line-height: 1.6;
1414
+ white-space: pre-wrap;
1415
+ word-break: break-word;
1416
+ color: #710909;
1417
+ margin: 0;
1418
+ }
1419
+ #qunit-testresult {
1420
+ padding: 0.5em 1em;
1421
+ color: #366097;
1422
+ background-color: #E2F0F7;
1423
+ border-bottom: 1px solid #fff;
1424
+ font-size: small;
1425
+ }
1426
+ .dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
1427
+ .dots span:nth-child(2) { animation-delay: .2s; }
1428
+ .dots span:nth-child(3) { animation-delay: .4s; }
1429
+ @keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
1430
+ </style>
1431
+ </head>
1432
+ <body>
1433
+ <div id="qunit">
1434
+ <h1 id="qunit-header">qunitx</h1>
1435
+ <h2 id="qunit-banner"></h2>
1436
+ <div id="qunit-userAgent">Build Error: ${buildError.type}</div>
1437
+ <ol id="qunit-tests">
1438
+ <li class="fail">
1439
+ <strong>esbuild failed to bundle test files</strong>
1440
+ <ol class="qunit-assert-list">
1441
+ <li><pre>${escaped}</pre></li>
1442
+ </ol>
1443
+ </li>
1444
+ </ol>
1445
+ <div id="qunit-testresult">
1446
+ Watching for changes&nbsp;<span class="dots"><span>&#9679;</span><span>&#9679;</span><span>&#9679;</span></span>
1447
+ </div>
1448
+ </div>
1449
+ <script>
1450
+ if (location.port) {
1451
+ (function () {
1452
+ var retries = 0;
1453
+ function connect() {
1454
+ var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
1455
+ ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1456
+ ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1457
+ ws.addEventListener('error', function () { ws.close(); });
1458
+ }
1459
+ connect();
1460
+ })();
1461
+ }
1462
+ </script>
1463
+ </body>
1464
+ </html>`;
1465
+ }
1218
1466
  var fsPromise;
1219
1467
  var init_web_server = __esm({
1220
1468
  "lib/setup/web-server.ts"() {
@@ -1282,7 +1530,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1282
1530
  perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
1283
1531
  const browser = resolvedExistingBrowser || await launchBrowser(config);
1284
1532
  const pageStart = Date.now();
1285
- const [page] = await Promise.all([browser.newPage(), bindServerToPort(server, config)]);
1533
+ const isHeadedWatchMode = config.open === true && config.watch;
1534
+ const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
1535
+ const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
1286
1536
  perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
1287
1537
  await page.addInitScript(() => {
1288
1538
  window.IS_PLAYWRIGHT = true;
@@ -1388,7 +1638,7 @@ var init_run_user_module = __esm({
1388
1638
  });
1389
1639
 
1390
1640
  // lib/tap/display-final-result.ts
1391
- function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, timeTaken) {
1641
+ function TAPDisplayFinalResult({ testCount, passCount, skipCount, todoCount, failCount }, timeTaken) {
1392
1642
  process.stdout.write("\n");
1393
1643
  process.stdout.write(`1..${testCount}
1394
1644
  `);
@@ -1397,6 +1647,8 @@ function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, t
1397
1647
  process.stdout.write(`# pass ${passCount}
1398
1648
  `);
1399
1649
  process.stdout.write(`# skip ${skipCount}
1650
+ `);
1651
+ process.stdout.write(`# todo ${todoCount}
1400
1652
  `);
1401
1653
  process.stdout.write(`# fail ${failCount}
1402
1654
  `);
@@ -1411,7 +1663,36 @@ var init_display_final_result = __esm({
1411
1663
 
1412
1664
  // lib/commands/run/tests-in-browser.ts
1413
1665
  import fs9 from "node:fs/promises";
1666
+ import path5 from "node:path";
1414
1667
  import esbuild from "esbuild";
1668
+ function deriveBuildErrorType(error) {
1669
+ const msgs = error?.errors ?? [];
1670
+ const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
1671
+ if (/could not resolve|cannot find module|no such file/i.test(text))
1672
+ return "Module Resolution Error";
1673
+ if (/unexpected token|expected .* but found|unterminated/i.test(text)) return "Syntax Error";
1674
+ if (/is not (defined|a function)|cannot read prop/i.test(text)) return "Reference Error";
1675
+ return "Build Error";
1676
+ }
1677
+ function formatBuildErrors(error) {
1678
+ const msgs = error?.errors ?? [];
1679
+ if (msgs.length > 0) {
1680
+ return msgs.map((msg, i) => {
1681
+ const loc = msg.location;
1682
+ const lineNum = loc ? String(loc.line) : "";
1683
+ const pad = loc ? " ".repeat(lineNum.length) : "";
1684
+ const locationLines = loc ? [
1685
+ ` ${loc.file}:${loc.line}:${loc.column}`,
1686
+ ` ${lineNum} \u2502 ${loc.lineText}`,
1687
+ ` ${pad} \u2502 ${" ".repeat(loc.column)}${"~".repeat(Math.max(1, loc.length))}`
1688
+ ] : [];
1689
+ const noteLines = msg.notes.filter((n) => n.text).map((n) => ` Note: ${n.text}`);
1690
+ return [`[${i + 1}] ${msg.text}`].concat(locationLines, noteLines).join("\n");
1691
+ }).join("\n\n");
1692
+ }
1693
+ const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1694
+ return raw.replace(/\x1b\[[0-9;]*[mGKH]/g, "").replace(/\r\n/g, "\n");
1695
+ }
1415
1696
  async function buildTestBundle(config, cachedContent) {
1416
1697
  const { projectRoot, output } = config;
1417
1698
  const allTestFilePaths = Object.keys(config.fsTree);
@@ -1428,8 +1709,12 @@ async function buildTestBundle(config, cachedContent) {
1428
1709
  contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
1429
1710
  resolveDir: process.cwd()
1430
1711
  },
1712
+ // Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
1713
+ // packages from any node_modules on the ancestor chain of cwd — the same lookup
1714
+ // order Node itself uses when resolving require() from process.cwd().
1715
+ nodePaths: ancestorNodeModules(process.cwd()),
1431
1716
  bundle: true,
1432
- logLevel: "error",
1717
+ logLevel: "silent",
1433
1718
  outfile,
1434
1719
  keepNames: true,
1435
1720
  legalComments: "none",
@@ -1441,26 +1726,47 @@ async function buildTestBundle(config, cachedContent) {
1441
1726
  // all browsers and does not require changes to user test code.
1442
1727
  footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1443
1728
  };
1444
- const [allTestCode] = await Promise.all([
1445
- config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
1446
- Promise.all(
1447
- cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
1448
- const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
1449
- if (htmlPath !== "/") {
1450
- await fs9.rm(targetPath, { force: true, recursive: true });
1451
- await fs9.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1452
- }
1453
- })
1454
- )
1455
- ]);
1456
- cachedContent.allTestCode = allTestCode;
1729
+ cachedContent._buildError = null;
1730
+ cachedContent._noTestsWarning = null;
1731
+ try {
1732
+ const [allTestCode] = await Promise.all([
1733
+ config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
1734
+ Promise.all(
1735
+ cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
1736
+ const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
1737
+ if (htmlPath !== "/") {
1738
+ await fs9.rm(targetPath, { force: true, recursive: true });
1739
+ await fs9.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1740
+ }
1741
+ })
1742
+ )
1743
+ ]);
1744
+ cachedContent.allTestCode = allTestCode;
1745
+ } catch (error) {
1746
+ cachedContent._buildError = {
1747
+ type: deriveBuildErrorType(error),
1748
+ formatted: formatBuildErrors(error)
1749
+ };
1750
+ await fs9.writeFile(
1751
+ `${projectRoot}/${output}/index.html`,
1752
+ buildErrorHTML(cachedContent._buildError)
1753
+ );
1754
+ throw error;
1755
+ }
1457
1756
  }
1458
1757
  async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
1459
1758
  const { projectRoot, output } = config;
1460
1759
  const allTestFilePaths = Object.keys(config.fsTree);
1461
1760
  const runHasFilter = !!targetTestFilesToFilter;
1462
1761
  if (!config._groupMode) {
1463
- config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
1762
+ config.COUNTER = {
1763
+ testCount: 0,
1764
+ failCount: 0,
1765
+ skipCount: 0,
1766
+ todoCount: 0,
1767
+ passCount: 0,
1768
+ errorCount: 0
1769
+ };
1464
1770
  }
1465
1771
  config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
1466
1772
  try {
@@ -1492,6 +1798,20 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1492
1798
  }
1493
1799
  const TIME_TAKEN = TIME_COUNTER.stop();
1494
1800
  if (!config._groupMode) {
1801
+ if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
1802
+ const displayFiles = allTestFilePaths.map(
1803
+ (f) => f.startsWith(`${projectRoot}/`) ? f.slice(projectRoot.length + 1) : f
1804
+ );
1805
+ cachedContent._noTestsWarning = displayFiles;
1806
+ const fileWord = allTestFilePaths.length === 1 ? "file" : "files";
1807
+ console.log(
1808
+ `# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
1809
+ );
1810
+ fs9.writeFile(`${projectRoot}/${output}/index.html`, buildNoTestsHTML(displayFiles)).catch(
1811
+ () => {
1812
+ }
1813
+ );
1814
+ }
1495
1815
  TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
1496
1816
  if (config.after) {
1497
1817
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
@@ -1507,8 +1827,18 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1507
1827
  }
1508
1828
  } catch (error) {
1509
1829
  config.lastFailedTestFiles = config.lastRanTestFiles;
1510
- console.log(error);
1511
1830
  const exception = new BundleError(error);
1831
+ if (!cachedContent._buildError && error.errors?.length) {
1832
+ cachedContent._buildError = {
1833
+ type: deriveBuildErrorType(error),
1834
+ formatted: formatBuildErrors(error)
1835
+ };
1836
+ fs9.writeFile(
1837
+ `${projectRoot}/${output}/qunitx.html`,
1838
+ buildErrorHTML(cachedContent._buildError)
1839
+ ).catch(() => {
1840
+ });
1841
+ }
1512
1842
  if (config.watch) {
1513
1843
  console.log(`# ${exception}`);
1514
1844
  } else {
@@ -1526,8 +1856,9 @@ function buildFilteredTests(filteredTests, outputPath, config) {
1526
1856
  contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
1527
1857
  resolveDir: process.cwd()
1528
1858
  },
1859
+ nodePaths: ancestorNodeModules(process.cwd()),
1529
1860
  bundle: true,
1530
- logLevel: "error",
1861
+ logLevel: "silent",
1531
1862
  outfile: outputPath,
1532
1863
  legalComments: "none",
1533
1864
  target: esbuildTarget(config.browser),
@@ -1542,15 +1873,13 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
1542
1873
  const MAX_RETRIES = 3;
1543
1874
  const EMPTY_BUNDLE_THRESHOLD = 500;
1544
1875
  let { result, js } = await getContents();
1876
+ const initialSize = js.length;
1545
1877
  for (let retry = 1; retry <= MAX_RETRIES; retry++) {
1546
1878
  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
1879
  await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
1551
1880
  ({ result, js } = await getContents());
1552
1881
  }
1553
- if (js.length < EMPTY_BUNDLE_THRESHOLD) {
1882
+ if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
1554
1883
  console.log(
1555
1884
  `# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
1556
1885
  );
@@ -1641,13 +1970,15 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1641
1970
  config._resetTestTimeout = null;
1642
1971
  config._testRunDone = null;
1643
1972
  }
1644
- if (!QUNIT_RESULT || QUNIT_RESULT.totalTests === 0) {
1973
+ if (!QUNIT_RESULT) {
1645
1974
  if (targetError) console.log(targetError);
1646
1975
  const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
1647
1976
  console.log(`# TIMEOUT: ${wsReason}`);
1648
1977
  console.log("BROWSER: runtime error thrown during executing tests");
1649
1978
  console.error("BROWSER: runtime error thrown during executing tests");
1650
1979
  await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
1980
+ } else if (QUNIT_RESULT.totalTests === 0) {
1981
+ return;
1651
1982
  } else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
1652
1983
  if (targetError) console.log(targetError);
1653
1984
  console.log(
@@ -1673,7 +2004,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
1673
2004
  process.exit(1);
1674
2005
  }
1675
2006
  }
1676
- var BundleError;
2007
+ var BundleError, ancestorNodeModules;
1677
2008
  var init_tests_in_browser = __esm({
1678
2009
  "lib/commands/run/tests-in-browser.ts"() {
1679
2010
  init_color();
@@ -1681,6 +2012,7 @@ var init_tests_in_browser = __esm({
1681
2012
  init_time_counter();
1682
2013
  init_run_user_module();
1683
2014
  init_display_final_result();
2015
+ init_web_server();
1684
2016
  BundleError = class extends Error {
1685
2017
  constructor(message) {
1686
2018
  super(message);
@@ -1688,13 +2020,16 @@ var init_tests_in_browser = __esm({
1688
2020
  this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
1689
2021
  }
1690
2022
  };
2023
+ ancestorNodeModules = (dir) => dir.split(path5.sep).map(
2024
+ (_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
2025
+ );
1691
2026
  }
1692
2027
  });
1693
2028
 
1694
2029
  // lib/setup/file-watcher.ts
1695
2030
  import fs10 from "node:fs";
1696
2031
  import { stat, lstat } from "node:fs/promises";
1697
- import path5 from "node:path";
2032
+ import path6 from "node:path";
1698
2033
  function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
1699
2034
  const extensions = config.extensions || ["js", "ts"];
1700
2035
  const readyPromises = [];
@@ -1724,7 +2059,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1724
2059
  const lastChangeMs = {};
1725
2060
  const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
1726
2061
  if (!ready || !filename) return;
1727
- const fullPath = path5.join(watchPath, filename);
2062
+ const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
1728
2063
  if (eventType === "change") {
1729
2064
  if (!config._building) {
1730
2065
  const now = Date.now();
@@ -1749,8 +2084,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1749
2084
  }
1750
2085
  handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
1751
2086
  });
1752
- const parentDir = path5.dirname(watchPath);
1753
- const watchedBasename = path5.basename(watchPath);
2087
+ const parentDir = path6.dirname(watchPath);
2088
+ const watchedBasename = path6.basename(watchPath);
1754
2089
  let parentUnlinkFired = false;
1755
2090
  const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
1756
2091
  if (!ready || filename !== watchedBasename || eventType !== "rename") return;
@@ -1825,7 +2160,8 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
1825
2160
  "#",
1826
2161
  magenta().bold("==================================================================")
1827
2162
  );
1828
- console.log("#", colorEvent(event), filePath.split(config.projectRoot)[1]);
2163
+ const displayPath = filePath.startsWith(config.projectRoot) ? filePath.slice(config.projectRoot.length) : filePath;
2164
+ console.log("#", colorEvent(event), displayPath);
1829
2165
  console.log(
1830
2166
  "#",
1831
2167
  magenta().bold("==================================================================")
@@ -1852,13 +2188,13 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
1852
2188
  }
1853
2189
  });
1854
2190
  }
1855
- function mutateFSTree(fsTree, event, path6) {
2191
+ function mutateFSTree(fsTree, event, path7) {
1856
2192
  if (event === "add") {
1857
- fsTree[path6] = null;
2193
+ fsTree[path7] = null;
1858
2194
  } else if (event === "unlink") {
1859
- delete fsTree[path6];
2195
+ delete fsTree[path7];
1860
2196
  } else if (event === "unlinkDir") {
1861
- const dirPrefix = path6.endsWith("/") ? path6 : path6 + "/";
2197
+ const dirPrefix = path7.endsWith("/") ? path7 : path7 + "/";
1862
2198
  for (const treePath of Object.keys(fsTree)) {
1863
2199
  if (treePath.startsWith(dirPrefix)) delete fsTree[treePath];
1864
2200
  }
@@ -1990,14 +2326,18 @@ import { availableParallelism } from "node:os";
1990
2326
  async function run(config) {
1991
2327
  const cachedContent = await buildCachedContent(config, config.htmlPaths);
1992
2328
  if (config.watch) {
1993
- cachedContent._preBuildPromise = buildTestBundle(config, cachedContent);
2329
+ const preBuildPromise = buildTestBundle(config, cachedContent);
2330
+ preBuildPromise.catch(() => {
2331
+ });
2332
+ cachedContent._preBuildPromise = preBuildPromise;
1994
2333
  const [connections] = await Promise.all([
1995
2334
  setupBrowser(config, cachedContent),
1996
2335
  writeOutputStaticFiles(config, cachedContent)
1997
2336
  ]);
1998
2337
  config.expressApp = connections.server;
1999
2338
  setupKeyboardEvents(config, cachedContent, connections);
2000
- if (config.open) {
2339
+ const isHeadedWatchMode = config.open === true && config.watch;
2340
+ if (config.open && !isHeadedWatchMode) {
2001
2341
  void openOutputInBrowser(config);
2002
2342
  }
2003
2343
  if (config.before) {
@@ -2012,6 +2352,10 @@ async function run(config) {
2012
2352
  ]);
2013
2353
  throw error;
2014
2354
  }
2355
+ if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
2356
+ await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
2357
+ });
2358
+ }
2015
2359
  if (config.watch) {
2016
2360
  const { ready: watcherReady } = setupFileWatchers(
2017
2361
  config.testFileLookupPaths,
@@ -2035,7 +2379,13 @@ async function run(config) {
2035
2379
  }
2036
2380
  await runTestsInBrowser(config, cachedContent, connections, [file]);
2037
2381
  },
2038
- (_path, _event) => connections.server.publish("refresh")
2382
+ async (_path, _event) => {
2383
+ connections.server.publish("refresh");
2384
+ if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
2385
+ await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
2386
+ });
2387
+ }
2388
+ }
2039
2389
  );
2040
2390
  await watcherReady;
2041
2391
  }
@@ -2044,7 +2394,14 @@ async function run(config) {
2044
2394
  const allFiles = Object.keys(config.fsTree);
2045
2395
  const groupCount = Math.min(allFiles.length, availableParallelism());
2046
2396
  const groups = splitIntoGroups(allFiles, groupCount);
2047
- config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
2397
+ config.COUNTER = {
2398
+ testCount: 0,
2399
+ failCount: 0,
2400
+ skipCount: 0,
2401
+ todoCount: 0,
2402
+ passCount: 0,
2403
+ errorCount: 0
2404
+ };
2048
2405
  config.lastRanTestFiles = allFiles;
2049
2406
  const groupConfigs = groups.map((groupFiles, i) => ({
2050
2407
  ...config,
@@ -2133,6 +2490,12 @@ async function run(config) {
2133
2490
  config.COUNTER.failCount > 0 ? 1 : 0
2134
2491
  );
2135
2492
  process.exitCode = exitCode;
2493
+ if (config.COUNTER.testCount === 0 && exitCode === 0) {
2494
+ const fileWord = allFiles.length === 1 ? "file" : "files";
2495
+ console.log(
2496
+ `# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allFiles.length} ${fileWord}`
2497
+ );
2498
+ }
2136
2499
  TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
2137
2500
  if (config.after) {
2138
2501
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
@@ -2256,7 +2619,7 @@ init_color();
2256
2619
  var package_default = {
2257
2620
  name: "qunitx-cli",
2258
2621
  type: "module",
2259
- version: "0.18.0",
2622
+ version: "0.19.0",
2260
2623
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2261
2624
  author: "Izel Nakri",
2262
2625
  license: "MIT",
@@ -2488,17 +2851,17 @@ function pathToModuleName(filePath) {
2488
2851
  async function generateTestFiles() {
2489
2852
  const projectRoot = await findProjectRoot();
2490
2853
  const moduleName = pathToModuleName(process.argv[3]);
2491
- const path6 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
2492
- if (await pathExists(path6)) {
2493
- console.log(`${path6} already exists!`);
2854
+ const path7 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
2855
+ if (await pathExists(path7)) {
2856
+ console.log(`${path7} already exists!`);
2494
2857
  return;
2495
2858
  }
2496
2859
  const testJSContent = await readTemplate("test.js");
2497
- const targetFolderPaths = path6.split("/");
2860
+ const targetFolderPaths = path7.split("/");
2498
2861
  targetFolderPaths.pop();
2499
2862
  await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
2500
- await fs5.writeFile(path6, testJSContent.replace("{{moduleName}}", moduleName));
2501
- console.log(green(`${path6} written`));
2863
+ await fs5.writeFile(path7, testJSContent.replace("{{moduleName}}", moduleName));
2864
+ console.log(green(`${path7} written`));
2502
2865
  }
2503
2866
 
2504
2867
  // lib/setup/config.ts
@@ -2599,20 +2962,20 @@ function setupTestFilePaths(_projectRoot, inputs2) {
2599
2962
  });
2600
2963
  return result.map((metaItem) => metaItem.input);
2601
2964
  }
2602
- function pathIsFile(path6) {
2603
- const inputs2 = path6.split("/");
2965
+ function pathIsFile(path7) {
2966
+ const inputs2 = path7.split("/");
2604
2967
  return inputs2[inputs2.length - 1].includes(".");
2605
2968
  }
2606
2969
  function pathIsIncludedInPaths(paths, targetPath) {
2607
- return paths.some((path6) => {
2608
- if (path6 === targetPath) {
2970
+ return paths.some((path7) => {
2971
+ if (path7 === targetPath) {
2609
2972
  return false;
2610
2973
  }
2611
- return matchesGlob(targetPath.input, buildGlobFormat(path6));
2974
+ return matchesGlob(targetPath.input, buildGlobFormat(path7));
2612
2975
  });
2613
2976
  }
2614
- function buildGlobFormat(path6) {
2615
- return path6.isFile ? path6.input : `${path6.input}/**`;
2977
+ function buildGlobFormat(path7) {
2978
+ return path7.isFile ? path7.input : `${path7.input}/**`;
2616
2979
  }
2617
2980
 
2618
2981
  // lib/utils/parse-cli-flags.ts
@@ -2666,7 +3029,9 @@ function parseCliFlags(projectRoot) {
2666
3029
  console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
2667
3030
  return result;
2668
3031
  }
2669
- result.inputs.add(arg.startsWith(projectRoot) ? arg : `${process.cwd()}/${arg}`);
3032
+ result.inputs.add(
3033
+ arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : `${process.cwd()}/${arg}`
3034
+ );
2670
3035
  return result;
2671
3036
  },
2672
3037
  { inputs: /* @__PURE__ */ new Set([]) }
@@ -2704,7 +3069,14 @@ async function setupConfig() {
2704
3069
  testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
2705
3070
  lastFailedTestFiles: null,
2706
3071
  lastRanTestFiles: null,
2707
- COUNTER: { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 },
3072
+ COUNTER: {
3073
+ testCount: 0,
3074
+ failCount: 0,
3075
+ skipCount: 0,
3076
+ todoCount: 0,
3077
+ passCount: 0,
3078
+ errorCount: 0
3079
+ },
2708
3080
  _testRunDone: null,
2709
3081
  _resetTestTimeout: null,
2710
3082
  _onWsOpen: null,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",