qunitx-cli 0.19.3 → 0.20.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 +383 -100
  2. package/package.json +1 -3
package/dist/cli.js CHANGED
@@ -92,12 +92,17 @@ async function cleanupBrowserDir(dirPath) {
92
92
  }
93
93
  const dirName = dirPath.split("/").pop();
94
94
  await killAllReferencingProcesses(dirPath, dirName);
95
- const deadline = Date.now() + 5e3;
95
+ const deadline = Date.now() + CLEANUP_DEADLINE_MS;
96
96
  while (Date.now() < deadline) {
97
- const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(() => true).catch(() => false);
97
+ const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
98
+ () => fs.access(dirPath).then(
99
+ () => false,
100
+ () => true
101
+ )
102
+ ).catch(() => false);
98
103
  if (removed) return;
99
104
  await killAllReferencingProcesses(dirPath, dirName);
100
- await new Promise((resolve) => setTimeout(resolve, 50));
105
+ await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
101
106
  }
102
107
  if (!await fs.access(dirPath).then(() => true).catch(() => false))
103
108
  return;
@@ -117,8 +122,11 @@ async function cleanupBrowserDir(dirPath) {
117
122
  })
118
123
  );
119
124
  }
125
+ var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
120
126
  var init_cleanup_browser_dir = __esm({
121
127
  "lib/utils/cleanup-browser-dir.ts"() {
128
+ CLEANUP_DEADLINE_MS = 5e3;
129
+ CLEANUP_RETRY_MS = 50;
122
130
  }
123
131
  });
124
132
 
@@ -472,10 +480,11 @@ function dumpYaml({
472
480
  expected,
473
481
  message,
474
482
  stack,
483
+ source,
475
484
  at
476
485
  }) {
477
486
  return `name: ${dumpString(name, "")}
478
- ` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (at !== null ? yamlLine("at", at) : "");
487
+ ` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (source !== null ? yamlLine("source", source) : "") + (at !== null ? yamlLine("at", at) : "");
479
488
  }
480
489
  var NEEDS_QUOTING;
481
490
  var init_dump_yaml = __esm({
@@ -498,8 +507,205 @@ var init_indent_string = __esm({
498
507
  }
499
508
  });
500
509
 
510
+ // lib/utils/source-map-decoder.ts
511
+ function readVLQ(s, pos) {
512
+ let accumulated = 0;
513
+ let shift = 0;
514
+ let digit;
515
+ do {
516
+ digit = BASE64_LOOKUP[s.charCodeAt(pos++)];
517
+ accumulated |= (digit & 31) << shift;
518
+ shift += 5;
519
+ } while (digit & 32);
520
+ return [accumulated & 1 ? -(accumulated >>> 1) : accumulated >>> 1, pos];
521
+ }
522
+ function decodeMappings(mappings) {
523
+ let sourceIndex = 0, sourceLine = 0, sourceCol = 0;
524
+ return mappings.split(";").map((lineStr) => {
525
+ const segments = [];
526
+ let generatedCol = 0;
527
+ let pos = 0;
528
+ while (pos < lineStr.length) {
529
+ if (lineStr[pos] === ",") {
530
+ pos++;
531
+ continue;
532
+ }
533
+ let delta;
534
+ [delta, pos] = readVLQ(lineStr, pos);
535
+ generatedCol += delta;
536
+ if (pos >= lineStr.length || lineStr[pos] === ",") continue;
537
+ [delta, pos] = readVLQ(lineStr, pos);
538
+ sourceIndex += delta;
539
+ [delta, pos] = readVLQ(lineStr, pos);
540
+ sourceLine += delta;
541
+ [delta, pos] = readVLQ(lineStr, pos);
542
+ sourceCol += delta;
543
+ if (pos < lineStr.length && lineStr[pos] !== ",") [, pos] = readVLQ(lineStr, pos);
544
+ segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
545
+ }
546
+ return segments;
547
+ });
548
+ }
549
+ function parseSourceMap(json, outDir) {
550
+ const map = JSON.parse(json);
551
+ return {
552
+ segmentsByLine: decodeMappings(map.mappings),
553
+ sources: map.sources ?? [],
554
+ sourceRoot: map.sourceRoot ?? "",
555
+ outDir,
556
+ sourcesContent: map.sourcesContent ?? []
557
+ };
558
+ }
559
+ function base64DecodeUtf8(b64) {
560
+ const binary = atob(b64);
561
+ return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
562
+ }
563
+ function extractInlineSourceMap(bundle, outDir) {
564
+ if (!bundle) return null;
565
+ const text = typeof bundle === "string" ? bundle : UTF8.decode(bundle);
566
+ const match = text.match(
567
+ /\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
568
+ );
569
+ if (!match) return null;
570
+ try {
571
+ return parseSourceMap(base64DecodeUtf8(match[1]), outDir);
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ function normalizePosix(p) {
577
+ const abs = p.startsWith("/");
578
+ const parts = p.split("/");
579
+ const out = [];
580
+ for (const part of parts) {
581
+ if (part === "..") out.pop();
582
+ else if (part !== "" && part !== ".") out.push(part);
583
+ }
584
+ return (abs ? "/" : "") + out.join("/");
585
+ }
586
+ function posixResolve(base, relative) {
587
+ if (relative.startsWith("/")) return normalizePosix(relative);
588
+ return normalizePosix(base + "/" + relative);
589
+ }
590
+ function toAbsolutePath(raw, outDir, sourceRoot) {
591
+ if (raw.startsWith("file://")) return raw.slice(7);
592
+ if (raw.startsWith("/")) return raw;
593
+ const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
594
+ return posixResolve(base, raw);
595
+ }
596
+ function lookupPosition(decoder, generatedLine, generatedCol) {
597
+ const segments = decoder.segmentsByLine[generatedLine - 1];
598
+ if (!segments?.length) return null;
599
+ const col0 = generatedCol - 1;
600
+ let lo = 0, hi = segments.length - 1, best = -1;
601
+ while (lo <= hi) {
602
+ const mid = lo + hi >>> 1;
603
+ if (segments[mid].generatedCol <= col0) {
604
+ best = mid;
605
+ lo = mid + 1;
606
+ } else {
607
+ hi = mid - 1;
608
+ }
609
+ }
610
+ if (best === -1) return null;
611
+ const { sourceIndex, sourceLine, sourceCol } = segments[best];
612
+ const rawSource = decoder.sources[sourceIndex];
613
+ if (!rawSource) return null;
614
+ const content = decoder.sourcesContent[sourceIndex];
615
+ const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
616
+ return {
617
+ absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
618
+ line: sourceLine + 1,
619
+ // 0-based → 1-based
620
+ col: sourceCol + 1,
621
+ // 0-based → 1-based
622
+ sourceText
623
+ };
624
+ }
625
+ function parseFrameLocation(s) {
626
+ const colSep = s.lastIndexOf(":");
627
+ if (colSep < 0) return null;
628
+ const colStr = s.slice(colSep + 1);
629
+ if (!/^\d+$/.test(colStr)) return null;
630
+ const lineSep = s.lastIndexOf(":", colSep - 1);
631
+ if (lineSep < 0) return null;
632
+ const lineStr = s.slice(lineSep + 1, colSep);
633
+ if (!/^\d+$/.test(lineStr)) return null;
634
+ return { url: s.slice(0, lineSep), line: +lineStr, col: +colStr };
635
+ }
636
+ function isBundleUrl(url) {
637
+ const normalized = url.startsWith("async ") ? url.slice(6) : url;
638
+ return /^https?:\/\//.test(normalized) && /\/(tests|filtered-tests)\.js$/.test(normalized);
639
+ }
640
+ function isNodeModulesPath(absolutePath) {
641
+ return absolutePath.includes("/node_modules/") || absolutePath.includes("\\node_modules\\");
642
+ }
643
+ function makeDisplayPath(absolutePath, projectRoot) {
644
+ const prefix = projectRoot + "/";
645
+ return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
646
+ }
647
+ function tryResolve(urlLineCol, decoder, projectRoot) {
648
+ const loc = parseFrameLocation(urlLineCol);
649
+ if (!loc || !isBundleUrl(loc.url)) return null;
650
+ const orig = lookupPosition(decoder, loc.line, loc.col);
651
+ if (!orig) return null;
652
+ const display = `${makeDisplayPath(orig.absolutePath, projectRoot)}:${orig.line}:${orig.col}`;
653
+ return {
654
+ display,
655
+ userPath: isNodeModulesPath(orig.absolutePath) ? null : display,
656
+ sourceText: orig.sourceText
657
+ };
658
+ }
659
+ function resolveFrame(frame, decoder, projectRoot) {
660
+ const chromeName = frame.match(/^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/);
661
+ if (chromeName) {
662
+ const r = tryResolve(chromeName[3], decoder, projectRoot);
663
+ return r ? {
664
+ resolved: `${chromeName[1]}${chromeName[2]} (${r.display})`,
665
+ userPath: r.userPath,
666
+ sourceText: r.sourceText
667
+ } : null;
668
+ }
669
+ const chromeAnon = frame.match(/^(\s*at\s+(?:async\s+)?)(.+)/);
670
+ if (chromeAnon) {
671
+ const r = tryResolve(chromeAnon[2], decoder, projectRoot);
672
+ return r ? { resolved: `${chromeAnon[1]}${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
673
+ }
674
+ const gecko = frame.match(/^([^@]*)@(.+)$/);
675
+ if (gecko) {
676
+ const r = tryResolve(gecko[2], decoder, projectRoot);
677
+ return r ? { resolved: `${gecko[1]}@${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
678
+ }
679
+ return null;
680
+ }
681
+ function resolveStack(stack, decoder, projectRoot) {
682
+ let firstUserFrame = null;
683
+ let firstUserSourceText = null;
684
+ const resolvedLines = stack.split("\n").map((frame) => {
685
+ const result = resolveFrame(frame, decoder, projectRoot);
686
+ if (!result) return frame;
687
+ if (!firstUserFrame && result.userPath) {
688
+ firstUserFrame = result.userPath;
689
+ firstUserSourceText = result.sourceText;
690
+ }
691
+ return result.resolved;
692
+ });
693
+ return { resolvedStack: resolvedLines.join("\n"), firstUserFrame, firstUserSourceText };
694
+ }
695
+ var BASE64, BASE64_LOOKUP, UTF8;
696
+ var init_source_map_decoder = __esm({
697
+ "lib/utils/source-map-decoder.ts"() {
698
+ BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
699
+ BASE64_LOOKUP = new Uint8Array(128);
700
+ [...BASE64].forEach((ch, i) => {
701
+ BASE64_LOOKUP[ch.charCodeAt(0)] = i;
702
+ });
703
+ UTF8 = new TextDecoder();
704
+ }
705
+ });
706
+
501
707
  // lib/tap/display-test-result.ts
502
- function TAPDisplayTestResult(COUNTER, details) {
708
+ function TAPDisplayTestResult(COUNTER, details, decoder, projectRoot) {
503
709
  COUNTER.testCount++;
504
710
  if (details.status === "skipped") {
505
711
  COUNTER.skipCount++;
@@ -519,6 +725,19 @@ function TAPDisplayTestResult(COUNTER, details) {
519
725
  if (!assertion.passed && assertion.todo === false) {
520
726
  COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
521
727
  process.stdout.write(" ---\n");
728
+ let stackStr = assertion.stack?.trim() || null;
729
+ let atStr = extractStackAt(assertion.stack);
730
+ let sourceText = null;
731
+ if (decoder && projectRoot && assertion.stack) {
732
+ const { resolvedStack, firstUserFrame, firstUserSourceText } = resolveStack(
733
+ assertion.stack,
734
+ decoder,
735
+ projectRoot
736
+ );
737
+ stackStr = resolvedStack.trim() || null;
738
+ atStr = firstUserFrame;
739
+ sourceText = firstUserSourceText;
740
+ }
522
741
  process.stdout.write(
523
742
  indentString(
524
743
  dumpYaml({
@@ -526,10 +745,9 @@ function TAPDisplayTestResult(COUNTER, details) {
526
745
  actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
527
746
  expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
528
747
  message: assertion.message || null,
529
- // Trim leading/trailing whitespace: Chrome stacks start with " at ..."
530
- // (4 spaces per frame) which would otherwise render as "stack: at ..." in YAML.
531
- stack: assertion.stack?.trim() || null,
532
- at: extractStackAt(assertion.stack)
748
+ stack: stackStr,
749
+ source: sourceText,
750
+ at: atStr
533
751
  }),
534
752
  4
535
753
  )
@@ -573,6 +791,7 @@ var init_display_test_result = __esm({
573
791
  "lib/tap/display-test-result.ts"() {
574
792
  init_dump_yaml();
575
793
  init_indent_string();
794
+ init_source_map_decoder();
576
795
  }
577
796
  });
578
797
 
@@ -812,7 +1031,7 @@ var init_http = __esm({
812
1031
  return true;
813
1032
  }
814
1033
  return false;
815
- }) || routes["/*"] || null;
1034
+ }) || null;
816
1035
  }
817
1036
  #matchPathSegments(path7, url) {
818
1037
  const pathSegments = path7.split("/");
@@ -902,7 +1121,7 @@ function setupWebServer(config, cachedContent) {
902
1121
  );
903
1122
  }
904
1123
  config._resetTestTimeout?.();
905
- TAPDisplayTestResult(config.COUNTER, details);
1124
+ TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
906
1125
  } else if (event === "done") {
907
1126
  config._phase = "done";
908
1127
  config._lastQUnitResult = qunitResult ?? null;
@@ -919,7 +1138,26 @@ function setupWebServer(config, cachedContent) {
919
1138
  }
920
1139
  });
921
1140
  });
922
- server.get("/tests.js", (_req, res) => {
1141
+ server.get("/tests.js", async (_req, res) => {
1142
+ if (cachedContent._activeRebuild) {
1143
+ await cachedContent._activeRebuild.catch(() => {
1144
+ });
1145
+ if (!cachedContent.allTestCode) {
1146
+ config._lastQUnitResult = {
1147
+ totalTests: 0,
1148
+ finishedTests: 0,
1149
+ failedTests: 0,
1150
+ currentTest: null
1151
+ };
1152
+ config._testRunDone?.();
1153
+ config._testRunDone = null;
1154
+ res.writeHead(200, {
1155
+ "Content-Type": "application/javascript",
1156
+ "Cache-Control": "no-store"
1157
+ });
1158
+ return void res.end();
1159
+ }
1160
+ }
923
1161
  const bytes = cachedContent.allTestCode?.length ?? null;
924
1162
  config.debug && process.stdout.write(
925
1163
  `# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
@@ -927,10 +1165,9 @@ function setupWebServer(config, cachedContent) {
927
1165
  );
928
1166
  if (bytes === null) {
929
1167
  res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
930
- res.end(
1168
+ return void res.end(
931
1169
  'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
932
1170
  );
933
- return;
934
1171
  }
935
1172
  config._onTestsJsServed?.();
936
1173
  res.writeHead(200, {
@@ -948,10 +1185,9 @@ function setupWebServer(config, cachedContent) {
948
1185
  );
949
1186
  if (bytes === null) {
950
1187
  res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
951
- res.end(
1188
+ return res.end(
952
1189
  'console.error("[qunitx] /filtered-tests.js requested before bundle was built \u2014 filteredTestCode is null");'
953
1190
  );
954
- return;
955
1191
  }
956
1192
  config._onTestsJsServed?.();
957
1193
  res.writeHead(200, {
@@ -961,18 +1197,28 @@ function setupWebServer(config, cachedContent) {
961
1197
  });
962
1198
  res.end(cachedContent.filteredTestCode);
963
1199
  });
964
- server.get("/", (_req, res) => {
1200
+ server.get("/", async (_req, res) => {
1201
+ await cachedContent._activeRebuild?.catch(() => {
1202
+ });
965
1203
  if (cachedContent._buildError) {
966
1204
  const htmlContent = buildErrorHTML(cachedContent._buildError);
967
1205
  res.writeHead(200, HTML_HEADERS);
968
1206
  res.end(htmlContent);
969
- saveHTML(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
970
- return;
1207
+ if (cachedContent._activeRebuild) {
1208
+ config._lastQUnitResult = {
1209
+ totalTests: 0,
1210
+ finishedTests: 0,
1211
+ failedTests: 0,
1212
+ currentTest: null
1213
+ };
1214
+ config._testRunDone?.();
1215
+ config._testRunDone = null;
1216
+ }
1217
+ return saveHTML(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
971
1218
  }
972
1219
  if (cachedContent._noTestsWarning) {
973
1220
  res.writeHead(200, HTML_HEADERS);
974
- res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
975
- return;
1221
+ return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
976
1222
  }
977
1223
  res.writeHead(200, HTML_HEADERS);
978
1224
  res.end(mainIndexHTML);
@@ -983,13 +1229,11 @@ function setupWebServer(config, cachedContent) {
983
1229
  const htmlContent = buildErrorHTML(cachedContent._buildError);
984
1230
  res.writeHead(200, HTML_HEADERS);
985
1231
  res.end(htmlContent);
986
- saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
987
- return;
1232
+ return saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
988
1233
  }
989
1234
  if (cachedContent._noTestsWarning) {
990
1235
  res.writeHead(200, HTML_HEADERS);
991
- res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
992
- return;
1236
+ return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
993
1237
  }
994
1238
  res.writeHead(200, HTML_HEADERS);
995
1239
  res.end(mainQunitxHTML);
@@ -1043,11 +1287,6 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
1043
1287
  function testRuntimeToInject(config, groupId) {
1044
1288
  const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
1045
1289
  return `<script>
1046
- window.testTimeout = 0;
1047
- setInterval(() => {
1048
- window.testTimeout = window.testTimeout + 1000;
1049
- }, 1000);
1050
-
1051
1290
  (function() {
1052
1291
  // wsOpenStatus: true once the WebSocket 'open' event fires (or immediately for static files).
1053
1292
  // testsLoaded: true once tests.js has executed and dispatched 'qunitx:tests-ready'.
@@ -1073,7 +1312,7 @@ function testRuntimeToInject(config, groupId) {
1073
1312
  if (window.location.protocol === 'file:') return;
1074
1313
 
1075
1314
  let wsRetryCount = 0;
1076
- const WS_MAX_RETRIES = Math.ceil(${config.timeout} / 10); // retry for the full test timeout window
1315
+ const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
1077
1316
 
1078
1317
  function setupWebSocket() {
1079
1318
  try {
@@ -1112,10 +1351,9 @@ function testRuntimeToInject(config, groupId) {
1112
1351
  wsRetryCount++;
1113
1352
  if (wsRetryCount > WS_MAX_RETRIES) {
1114
1353
  console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
1115
- window.testTimeout = ${config.timeout};
1116
1354
  return;
1117
1355
  }
1118
- window.setTimeout(setupWebSocket, 10);
1356
+ window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
1119
1357
  }
1120
1358
 
1121
1359
  setupWebSocket();
@@ -1149,8 +1387,6 @@ function testRuntimeToInject(config, groupId) {
1149
1387
  // "no tests registered" warning (not a failure), so this gives a fast, clean result.
1150
1388
  window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
1151
1389
  window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
1152
- } else {
1153
- window.testTimeout = ${config.timeout};
1154
1390
  }
1155
1391
  return;
1156
1392
  }
@@ -1165,7 +1401,6 @@ function testRuntimeToInject(config, groupId) {
1165
1401
  window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
1166
1402
  });
1167
1403
  window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
1168
- window.testTimeout = 0;
1169
1404
  window.QUNIT_RESULT.finishedTests++;
1170
1405
  if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
1171
1406
  window.QUNIT_RESULT.currentTest = null;
@@ -1182,16 +1417,10 @@ function testRuntimeToInject(config, groupId) {
1182
1417
  window.QUnit.done((details) => {
1183
1418
  if (navigator.webdriver) {
1184
1419
  window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
1185
- // Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
1186
- // canonical completion signal for Playwright runs. waitForFunction is reserved
1187
- // for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
1188
- // Setting testTimeout after done caused a race: under CI load, waitForFunction could
1189
- // win before Node.js processed the WS done message, dropping all testEnd events.
1190
- } else {
1191
- window.testTimeout = ${config.timeout};
1192
1420
  }
1193
1421
  });
1194
1422
 
1423
+ window.QUnit.config.testTimeout = ${config.timeout};
1195
1424
  window.QUnit.start();
1196
1425
  }
1197
1426
  </script>`;
@@ -1292,8 +1521,8 @@ function buildNoTestsHTML(files) {
1292
1521
  var retries = 0;
1293
1522
  function connect() {
1294
1523
  var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
1295
- ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1296
- ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1524
+ ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
1525
+ ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
1297
1526
  ws.addEventListener('error', function () { ws.close(); });
1298
1527
  }
1299
1528
  connect();
@@ -1394,8 +1623,8 @@ function buildErrorHTML(buildError) {
1394
1623
  var retries = 0;
1395
1624
  function connect() {
1396
1625
  var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
1397
- ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1398
- ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1626
+ ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
1627
+ ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
1399
1628
  ws.addEventListener('error', function () { ws.close(); });
1400
1629
  }
1401
1630
  connect();
@@ -1424,13 +1653,11 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
1424
1653
  server.get(`/group-${groupId}/`, (_req, res) => {
1425
1654
  if (groupCachedContent._buildError) {
1426
1655
  res.writeHead(200, HTML_HEADERS);
1427
- res.end(buildErrorHTML(groupCachedContent._buildError));
1428
- return;
1656
+ return res.end(buildErrorHTML(groupCachedContent._buildError));
1429
1657
  }
1430
1658
  if (groupCachedContent._noTestsWarning) {
1431
1659
  res.writeHead(200, HTML_HEADERS);
1432
- res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
1433
- return;
1660
+ return res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
1434
1661
  }
1435
1662
  res.writeHead(200, HTML_HEADERS);
1436
1663
  res.end(mainGroupHTML);
@@ -1440,10 +1667,9 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
1440
1667
  const bytes = groupCachedContent.allTestCode?.length ?? null;
1441
1668
  if (bytes === null) {
1442
1669
  res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
1443
- res.end(
1670
+ return res.end(
1444
1671
  'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
1445
1672
  );
1446
- return;
1447
1673
  }
1448
1674
  groupConfig._onTestsJsServed?.();
1449
1675
  res.writeHead(200, {
@@ -1495,7 +1721,7 @@ function setupGroupWSHandler(server, groupConfigs) {
1495
1721
  );
1496
1722
  }
1497
1723
  config._resetTestTimeout?.();
1498
- TAPDisplayTestResult(config.COUNTER, details);
1724
+ TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
1499
1725
  } else if (event === "done") {
1500
1726
  config._phase = "done";
1501
1727
  config._lastQUnitResult = qunitResult ?? null;
@@ -1544,7 +1770,7 @@ function registerSharedStaticHandler(server, groupConfigs) {
1544
1770
  });
1545
1771
  });
1546
1772
  }
1547
- var fsPromise, HTML_HEADERS, NOT_FOUND_HTML;
1773
+ var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
1548
1774
  var init_web_server = __esm({
1549
1775
  "lib/setup/web-server.ts"() {
1550
1776
  init_find_internal_assets_from_html();
@@ -1554,6 +1780,9 @@ var init_web_server = __esm({
1554
1780
  init_http();
1555
1781
  fsPromise = fs8.promises;
1556
1782
  HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
1783
+ WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
1784
+ WATCH_WS_RECONNECT_MAX_RETRIES = 120;
1785
+ WS_RETRY_INTERVAL_MS = 10;
1557
1786
  NOT_FOUND_HTML = `<!DOCTYPE html>
1558
1787
  <html lang="en">
1559
1788
  <head>
@@ -1637,12 +1866,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null, share
1637
1866
  perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
1638
1867
  return [sharedServer, existingBrowser, newPage2];
1639
1868
  }
1640
- const [newServer, resolvedBrowser] = await Promise.all([
1641
- setupWebServer(config, cachedContent),
1642
- Promise.resolve(existingBrowser)
1643
- ]);
1869
+ const newServer = setupWebServer(config, cachedContent);
1644
1870
  perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
1645
- const activeBrowser = resolvedBrowser ?? await launchBrowser(config);
1871
+ const activeBrowser = existingBrowser ?? await launchBrowser(config);
1646
1872
  const pageStart = Date.now();
1647
1873
  const isHeadedWatchMode = config.open === true && config.watch;
1648
1874
  const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
@@ -1836,7 +2062,7 @@ async function buildTestBundle(config, cachedContent) {
1836
2062
  }
1837
2063
  const outfile = `${projectRoot}/${output}/tests.js`;
1838
2064
  await fs9.mkdir(`${projectRoot}/${output}`, { recursive: true });
1839
- const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
2065
+ const sourcemap = "inline";
1840
2066
  const needsDisk = true;
1841
2067
  const buildOptions = {
1842
2068
  stdin: {
@@ -1876,6 +2102,7 @@ async function buildTestBundle(config, cachedContent) {
1876
2102
  )
1877
2103
  ]);
1878
2104
  cachedContent.allTestCode = allTestCode;
2105
+ config._sourceMapDecoder = extractInlineSourceMap(allTestCode, `${projectRoot}/${output}`);
1879
2106
  } catch (error) {
1880
2107
  cachedContent._buildError = {
1881
2108
  type: deriveBuildErrorType(error),
@@ -1907,9 +2134,13 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1907
2134
  const preBuildPromise = cachedContent._preBuildPromise;
1908
2135
  cachedContent._preBuildPromise = null;
1909
2136
  if (!cachedContent.allTestCode) {
1910
- await (preBuildPromise ?? buildTestBundle(config, cachedContent));
2137
+ if (preBuildPromise) {
2138
+ cachedContent._activeRebuild = preBuildPromise;
2139
+ } else {
2140
+ await buildTestBundle(config, cachedContent);
2141
+ }
1911
2142
  }
1912
- if (!cachedContent.allTestCode) {
2143
+ if (!cachedContent.allTestCode && !cachedContent._activeRebuild) {
1913
2144
  return connections;
1914
2145
  }
1915
2146
  if (runHasFilter) {
@@ -1919,6 +2150,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1919
2150
  outputPath,
1920
2151
  config
1921
2152
  );
2153
+ config._sourceMapDecoder = extractInlineSourceMap(
2154
+ cachedContent.filteredTestCode,
2155
+ `${projectRoot}/${output}`
2156
+ );
1922
2157
  }
1923
2158
  const TIME_COUNTER = timeCounter();
1924
2159
  if (runHasFilter) {
@@ -1930,6 +2165,17 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1930
2165
  )
1931
2166
  );
1932
2167
  }
2168
+ if (cachedContent._activeRebuild) {
2169
+ await cachedContent._activeRebuild.catch(() => {
2170
+ });
2171
+ cachedContent._activeRebuild = null;
2172
+ if (!cachedContent.allTestCode) {
2173
+ config.watch && cachedContent._buildError && console.log(
2174
+ `# esbuild Bundle Error: ${cachedContent._buildError.formatted}`.split("\n").join("\n# ")
2175
+ );
2176
+ return connections;
2177
+ }
2178
+ }
1933
2179
  const TIME_TAKEN = TIME_COUNTER.stop();
1934
2180
  if (!config._groupMode) {
1935
2181
  if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
@@ -1961,6 +2207,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1961
2207
  }
1962
2208
  }
1963
2209
  } catch (error) {
2210
+ cachedContent._activeRebuild = null;
1964
2211
  config.lastFailedTestFiles = config.lastRanTestFiles;
1965
2212
  const exception = new BundleError(error);
1966
2213
  if (!cachedContent._buildError && error.errors?.length) {
@@ -1985,8 +2232,8 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1985
2232
  return connections;
1986
2233
  }
1987
2234
  function buildFilteredTests(filteredTests, outputPath, config) {
1988
- const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
1989
- const needsDisk = sourcemap === "linked" || Boolean(config.open);
2235
+ const sourcemap = "inline";
2236
+ const needsDisk = Boolean(config.open);
1990
2237
  return buildWithOverlayfsRetry(
1991
2238
  {
1992
2239
  stdin: {
@@ -2060,9 +2307,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
2060
2307
  let wsConnected = false;
2061
2308
  try {
2062
2309
  console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
2063
- const navMs = config.timeout + 1e4;
2064
- const startupMs = Math.max(config.timeout * 3, navMs);
2065
- const testsJsMs = Math.max(config.timeout * 4, navMs);
2310
+ const navMs = config.timeout + NAV_GRACE_MS;
2311
+ const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
2312
+ const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
2066
2313
  let resolveTestRace;
2067
2314
  const testRaceResult = new Promise((resolve) => {
2068
2315
  resolveTestRace = resolve;
@@ -2080,7 +2327,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
2080
2327
  config._resetTestTimeout = () => {
2081
2328
  wsConnected = true;
2082
2329
  clearTimeout(timeoutHandle);
2083
- timeoutHandle = setTimeout(resolveTestRace, config.timeout);
2330
+ timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
2084
2331
  };
2085
2332
  const targetUrl = `http://localhost:${config.port}${filePath}`;
2086
2333
  const navOptions = { timeout: navMs, waitUntil: "commit" };
@@ -2150,7 +2397,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
2150
2397
  process.exit(1);
2151
2398
  }
2152
2399
  }
2153
- async function flushConsoleHandlers(handlers, deadline = Date.now() + 2e3) {
2400
+ async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
2154
2401
  if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
2155
2402
  await Promise.allSettled([...handlers]);
2156
2403
  return flushConsoleHandlers(handlers, deadline);
@@ -2160,7 +2407,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2160
2407
  cachedContent._buildError = null;
2161
2408
  cachedContent._noTestsWarning = null;
2162
2409
  });
2163
- const { projectRoot, debug, watch, browser } = groupConfigs[0];
2410
+ const { projectRoot, debug, browser } = groupConfigs[0];
2164
2411
  const activeGroups = groupConfigs.reduce(
2165
2412
  (acc, groupConfig, groupIndex) => {
2166
2413
  const files = Object.keys(groupConfig.fsTree);
@@ -2184,7 +2431,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2184
2431
  (group) => fs9.mkdir(`${group.config.projectRoot}/${group.config.output}`, { recursive: true })
2185
2432
  )
2186
2433
  );
2187
- const sourcemap = debug ? "inline" : watch ? "linked" : false;
2434
+ const sourcemap = "inline";
2188
2435
  const groupEntryPlugin = {
2189
2436
  name: "group-entry-loader",
2190
2437
  setup(build) {
@@ -2201,6 +2448,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2201
2448
  });
2202
2449
  }
2203
2450
  };
2451
+ const esbuildOutdir = path5.join(projectRoot, "tmp");
2204
2452
  const buildOptions = {
2205
2453
  entryPoints: activeGroups.map((_, slotIndex) => ({
2206
2454
  in: `group-entry-${slotIndex}`,
@@ -2212,7 +2460,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2212
2460
  logLevel: "silent",
2213
2461
  // outdir only labels the paths in outputFiles[].path — nothing is written to disk
2214
2462
  // (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
2215
- outdir: path5.join(projectRoot, "tmp"),
2463
+ outdir: esbuildOutdir,
2216
2464
  keepNames: true,
2217
2465
  legalComments: "none",
2218
2466
  target: esbuildTarget(browser),
@@ -2239,7 +2487,13 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2239
2487
  const isMap = Boolean(match[2]);
2240
2488
  const { config, cachedContent } = activeGroups[slotIndex];
2241
2489
  const destPath = `${config.projectRoot}/${config.output}/tests.js${isMap ? ".map" : ""}`;
2242
- if (!isMap) cachedContent.allTestCode = Buffer.from(outputFile.contents);
2490
+ if (!isMap) {
2491
+ cachedContent.allTestCode = Buffer.from(outputFile.contents);
2492
+ config._sourceMapDecoder = extractInlineSourceMap(
2493
+ cachedContent.allTestCode,
2494
+ esbuildOutdir
2495
+ );
2496
+ }
2243
2497
  return fs9.writeFile(destPath, outputFile.contents);
2244
2498
  })
2245
2499
  );
@@ -2258,7 +2512,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2258
2512
  throw error;
2259
2513
  }
2260
2514
  }
2261
- var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, GROUP_OUTPUT_REGEX;
2515
+ var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX;
2262
2516
  var init_tests_in_browser = __esm({
2263
2517
  "lib/commands/run/tests-in-browser.ts"() {
2264
2518
  init_color();
@@ -2267,6 +2521,7 @@ var init_tests_in_browser = __esm({
2267
2521
  init_run_user_module();
2268
2522
  init_display_final_result();
2269
2523
  init_web_server();
2524
+ init_source_map_decoder();
2270
2525
  BundleError = class extends Error {
2271
2526
  constructor(message) {
2272
2527
  super(message);
@@ -2281,6 +2536,11 @@ var init_tests_in_browser = __esm({
2281
2536
  RETRY_DELAY_MS = 100;
2282
2537
  MAX_RETRIES = 3;
2283
2538
  EMPTY_BUNDLE_THRESHOLD = 500;
2539
+ NAV_GRACE_MS = 1e4;
2540
+ STARTUP_TIMEOUT_FACTOR = 3;
2541
+ TESTS_JS_TIMEOUT_FACTOR = 4;
2542
+ CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
2543
+ TEST_STALL_BUFFER_MS = 5e3;
2284
2544
  GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
2285
2545
  }
2286
2546
  });
@@ -2306,7 +2566,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2306
2566
  }
2307
2567
  }
2308
2568
  };
2309
- fs10.watchFile(filePath, { interval: 500, persistent: false }, handler);
2569
+ fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
2310
2570
  symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
2311
2571
  }
2312
2572
  function untrackSymlink(filePath) {
@@ -2320,13 +2580,17 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2320
2580
  if (!ready || !filename) return;
2321
2581
  const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
2322
2582
  if (eventType === "change") {
2323
- if (!config._building) {
2324
- const now = Date.now();
2325
- const last = lastChangeMs[fullPath] ?? 0;
2326
- if (now - last < CHANGE_DEDUPE_MS) {
2327
- if (!config._lastBuildEndMs || config._lastBuildEndMs <= last) return;
2583
+ const now = Date.now();
2584
+ const last = lastChangeMs[fullPath] ?? 0;
2585
+ lastChangeMs[fullPath] = now;
2586
+ if (now - last < CHANGE_DEDUPE_MS && (config._building || !config._lastBuildEndMs || config._lastBuildEndMs <= last))
2587
+ return;
2588
+ if (config._lastBuildEndMs) {
2589
+ try {
2590
+ const { mtimeMs } = await stat(fullPath);
2591
+ if (mtimeMs < Math.floor(config._lastBuildEndMs / 1e3) * 1e3) return;
2592
+ } catch {
2328
2593
  }
2329
- lastChangeMs[fullPath] = now;
2330
2594
  }
2331
2595
  return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
2332
2596
  }
@@ -2396,7 +2660,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2396
2660
  };
2397
2661
  }
2398
2662
  async function classifyRenameEvent(fullPath, fsTree) {
2399
- for (const delay of [0, 50]) {
2663
+ for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
2400
2664
  if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
2401
2665
  try {
2402
2666
  const statResult = await stat(fullPath);
@@ -2464,11 +2728,13 @@ function colorEvent(event) {
2464
2728
  if (event === "add" || event === "addDir") return green("ADDED:");
2465
2729
  return red("REMOVED:");
2466
2730
  }
2467
- var CHANGE_DEDUPE_MS;
2731
+ var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
2468
2732
  var init_file_watcher = __esm({
2469
2733
  "lib/setup/file-watcher.ts"() {
2470
2734
  init_color();
2471
2735
  CHANGE_DEDUPE_MS = 10;
2736
+ SYMLINK_POLL_INTERVAL_MS = 500;
2737
+ OVERLAYFS_RENAME_RETRY_MS = 50;
2472
2738
  }
2473
2739
  });
2474
2740
 
@@ -2585,7 +2851,11 @@ import fs12 from "node:fs/promises";
2585
2851
  import { normalize } from "node:path";
2586
2852
  import { availableParallelism } from "node:os";
2587
2853
  async function run(config) {
2588
- const cachedContent = await buildCachedContent(config, config.htmlPaths);
2854
+ const browserPromise = config.watch ? null : launchBrowser(config);
2855
+ const [cachedContent, timings] = await Promise.all([
2856
+ buildCachedContent(config, config.htmlPaths),
2857
+ config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
2858
+ ]);
2589
2859
  if (config.watch) {
2590
2860
  const preBuildPromise = buildTestBundle(config, cachedContent);
2591
2861
  preBuildPromise.catch(() => {
@@ -2595,7 +2865,7 @@ async function run(config) {
2595
2865
  setupBrowser(config, cachedContent),
2596
2866
  writeOutputStaticFiles(config, cachedContent)
2597
2867
  ]);
2598
- config.expressApp = connections.server;
2868
+ config.webServer = connections.server;
2599
2869
  setupKeyboardEvents(config, cachedContent, connections);
2600
2870
  const isHeadedWatchMode = config.open === true && config.watch;
2601
2871
  if (config.open && !isHeadedWatchMode) {
@@ -2614,7 +2884,10 @@ async function run(config) {
2614
2884
  throw error;
2615
2885
  }
2616
2886
  if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
2617
- await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
2887
+ await connections.page.goto(`http://localhost:${config.port}/`, {
2888
+ waitUntil: "commit",
2889
+ timeout: WATCH_NAV_TIMEOUT_MS
2890
+ }).catch(() => {
2618
2891
  });
2619
2892
  }
2620
2893
  if (config.watch) {
@@ -2631,6 +2904,10 @@ async function run(config) {
2631
2904
  `# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
2632
2905
  );
2633
2906
  }
2907
+ const rebuildPromise = buildTestBundle(config, cachedContent);
2908
+ rebuildPromise.catch(() => {
2909
+ });
2910
+ cachedContent._preBuildPromise = rebuildPromise;
2634
2911
  return await runTestsInBrowser(config, cachedContent, connections);
2635
2912
  }
2636
2913
  if (config.debug) {
@@ -2643,7 +2920,10 @@ async function run(config) {
2643
2920
  async (_path, _event) => {
2644
2921
  connections.server.publish("refresh");
2645
2922
  if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
2646
- await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
2923
+ await connections.page.goto(`http://localhost:${config.port}/`, {
2924
+ waitUntil: "commit",
2925
+ timeout: WATCH_NAV_TIMEOUT_MS
2926
+ }).catch(() => {
2647
2927
  });
2648
2928
  }
2649
2929
  }
@@ -2654,8 +2934,7 @@ async function run(config) {
2654
2934
  } else {
2655
2935
  const allFiles = Object.keys(config.fsTree);
2656
2936
  const groupCount = Math.min(allFiles.length, availableParallelism());
2657
- const timings = await readTimingCache(config.projectRoot);
2658
- const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
2937
+ const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
2659
2938
  config.COUNTER = {
2660
2939
  testCount: 0,
2661
2940
  failCount: 0,
@@ -2687,7 +2966,7 @@ async function run(config) {
2687
2966
  `
2688
2967
  );
2689
2968
  const [browser] = await Promise.all([
2690
- launchBrowser(config),
2969
+ browserPromise,
2691
2970
  sharedServer ? bindServerToPort(sharedServer, config).then(
2692
2971
  () => groupConfigs.forEach((gc, i) => {
2693
2972
  gc.port = config.port;
@@ -2708,7 +2987,7 @@ async function run(config) {
2708
2987
  const wallTimes = /* @__PURE__ */ new Map();
2709
2988
  const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
2710
2989
  const keepAlive = setInterval(() => {
2711
- }, 1e4);
2990
+ }, KEEP_ALIVE_INTERVAL_MS);
2712
2991
  const groupResults = await Promise.allSettled(
2713
2992
  groupConfigs.map((groupConfig, i) => {
2714
2993
  const groupTimeout = new Promise((_, reject) => {
@@ -2734,7 +3013,7 @@ async function run(config) {
2734
3013
  browser,
2735
3014
  sharedServer
2736
3015
  );
2737
- groupConfig.expressApp = connections.server;
3016
+ groupConfig.webServer = connections.server;
2738
3017
  if (config.before) {
2739
3018
  await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
2740
3019
  }
@@ -2749,7 +3028,7 @@ async function run(config) {
2749
3028
  Promise.race([
2750
3029
  connections.page.close(),
2751
3030
  new Promise((resolve) => {
2752
- const pageCloseTimeoutId = setTimeout(resolve, 1e4);
3031
+ const pageCloseTimeoutId = setTimeout(resolve, PAGE_CLOSE_GRACE_MS);
2753
3032
  pageCloseTimeoutId.unref();
2754
3033
  })
2755
3034
  ]).catch(() => {
@@ -2787,11 +3066,10 @@ async function run(config) {
2787
3066
  if (config.after) {
2788
3067
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
2789
3068
  }
2790
- const exitTimer = setTimeout(() => process.exit(exitCode), 5e3);
3069
+ const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
2791
3070
  exitTimer.unref();
2792
3071
  process.stdout.write("\n", async () => {
2793
3072
  clearTimeout(exitTimer);
2794
- clearInterval(keepAlive);
2795
3073
  await Promise.all([
2796
3074
  sharedServer?.close().catch(
2797
3075
  (err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
@@ -2803,6 +3081,7 @@ async function run(config) {
2803
3081
  )
2804
3082
  ]);
2805
3083
  await shutdownPrelaunch();
3084
+ clearInterval(keepAlive);
2806
3085
  process.exit(exitCode);
2807
3086
  });
2808
3087
  }
@@ -2931,6 +3210,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
2931
3210
  const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
2932
3211
  return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
2933
3212
  }
3213
+ var WATCH_NAV_TIMEOUT_MS, PAGE_CLOSE_GRACE_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS;
2934
3214
  var init_run = __esm({
2935
3215
  "lib/commands/run.ts"() {
2936
3216
  init_browser();
@@ -2950,6 +3230,10 @@ var init_run = __esm({
2950
3230
  init_display_final_result();
2951
3231
  init_read_template();
2952
3232
  init_html();
3233
+ WATCH_NAV_TIMEOUT_MS = 5e3;
3234
+ PAGE_CLOSE_GRACE_MS = 1e4;
3235
+ STDOUT_FLUSH_GRACE_MS = 5e3;
3236
+ KEEP_ALIVE_INTERVAL_MS = 1e4;
2953
3237
  }
2954
3238
  });
2955
3239
 
@@ -2964,7 +3248,7 @@ init_color();
2964
3248
  var package_default = {
2965
3249
  name: "qunitx-cli",
2966
3250
  type: "module",
2967
- version: "0.19.3",
3251
+ version: "0.20.0",
2968
3252
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2969
3253
  author: "Izel Nakri",
2970
3254
  license: "MIT",
@@ -3019,8 +3303,6 @@ var package_default = {
3019
3303
  ws: "^8.20.0"
3020
3304
  },
3021
3305
  devDependencies: {
3022
- cors: "^2.8.6",
3023
- express: "^5.2.1",
3024
3306
  "js-yaml": "^4.1.1",
3025
3307
  prettier: "^3.8.2",
3026
3308
  qunitx: "^1.2.7",
@@ -3330,6 +3612,7 @@ function buildGlobFormat(path7) {
3330
3612
  }
3331
3613
 
3332
3614
  // lib/utils/parse-cli-flags.ts
3615
+ var FALLBACK_TIMEOUT_MS = 1e4;
3333
3616
  function parseCliFlags(projectRoot) {
3334
3617
  const providedFlags = process.argv.slice(2).reduce(
3335
3618
  (result, arg) => {
@@ -3344,7 +3627,7 @@ function parseCliFlags(projectRoot) {
3344
3627
  } else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
3345
3628
  return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
3346
3629
  } else if (arg.startsWith("--timeout")) {
3347
- return Object.assign(result, { timeout: Number(arg.split("=")[1]) || 1e4 });
3630
+ return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
3348
3631
  } else if (arg.startsWith("--output")) {
3349
3632
  return Object.assign(result, { output: arg.split("=")[1] });
3350
3633
  } else if (arg.endsWith(".html")) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.19.3",
4
+ "version": "0.20.0",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -56,8 +56,6 @@
56
56
  "ws": "^8.20.0"
57
57
  },
58
58
  "devDependencies": {
59
- "cors": "^2.8.6",
60
- "express": "^5.2.1",
61
59
  "js-yaml": "^4.1.1",
62
60
  "prettier": "^3.8.2",
63
61
  "qunitx": "^1.2.7",