qunitx-cli 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +81 -37
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -93,7 +93,7 @@ async function cleanupBrowserDir(dirPath) {
93
93
  }
94
94
  }
95
95
  }
96
- const deadline = Date.now() + 1e3;
96
+ const deadline = Date.now() + 5e3;
97
97
  while (Date.now() < deadline) {
98
98
  const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(() => true).catch(() => false);
99
99
  if (removed) break;
@@ -908,7 +908,7 @@ function setupWebServer(config, cachedContent) {
908
908
  );
909
909
  server.wss.on("connection", function connection(socket) {
910
910
  socket.on("message", function message(data) {
911
- const { event, details, abort } = JSON.parse(data);
911
+ const { event, details, qunitResult, abort } = JSON.parse(data);
912
912
  if (event === "wsOpen") {
913
913
  config._phase = "loading";
914
914
  config._onWsOpen?.();
@@ -941,6 +941,7 @@ function setupWebServer(config, cachedContent) {
941
941
  TAPDisplayTestResult(config.COUNTER, details);
942
942
  } else if (event === "done") {
943
943
  config._phase = "done";
944
+ config._lastQUnitResult = qunitResult ?? null;
944
945
  if (config.debug && config._groupMode) {
945
946
  process.stdout.write(
946
947
  `# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
@@ -968,7 +969,11 @@ function setupWebServer(config, cachedContent) {
968
969
  return;
969
970
  }
970
971
  config._onTestsJsServed?.();
971
- res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
972
+ res.writeHead(200, {
973
+ "Content-Type": "application/javascript",
974
+ "Cache-Control": "no-store",
975
+ "Content-Length": bytes
976
+ });
972
977
  res.end(cachedContent.allTestCode);
973
978
  });
974
979
  server.get("/filtered-tests.js", (_req, res) => {
@@ -985,7 +990,11 @@ function setupWebServer(config, cachedContent) {
985
990
  return;
986
991
  }
987
992
  config._onTestsJsServed?.();
988
- res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
993
+ res.writeHead(200, {
994
+ "Content-Type": "application/javascript",
995
+ "Cache-Control": "no-store",
996
+ "Content-Length": bytes
997
+ });
989
998
  res.end(cachedContent.filteredTestCode);
990
999
  });
991
1000
  server.get("/", async (_req, res) => {
@@ -1006,10 +1015,9 @@ function setupWebServer(config, cachedContent) {
1006
1015
  res.end();
1007
1016
  return;
1008
1017
  }
1009
- const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
1010
1018
  const htmlContent = escapeAndInjectTestsToHTML(
1011
1019
  mainHTMLWithReplacedAssets,
1012
- TEST_RUNTIME_TO_INJECT,
1020
+ testRuntimeToInject(config.port, config),
1013
1021
  "./tests.js"
1014
1022
  );
1015
1023
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1038,10 +1046,9 @@ function setupWebServer(config, cachedContent) {
1038
1046
  res.end();
1039
1047
  return;
1040
1048
  }
1041
- const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
1042
1049
  const htmlContent = escapeAndInjectTestsToHTML(
1043
1050
  mainHTMLWithReplacedAssets,
1044
- TEST_RUNTIME_TO_INJECT,
1051
+ testRuntimeToInject(config.port, config),
1045
1052
  "./filtered-tests.js"
1046
1053
  );
1047
1054
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1055,10 +1062,9 @@ function setupWebServer(config, cachedContent) {
1055
1062
  server.get("/*", async (req, res) => {
1056
1063
  const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
1057
1064
  if (possibleDynamicHTML) {
1058
- const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
1059
1065
  const htmlContent = escapeAndInjectTestsToHTML(
1060
1066
  possibleDynamicHTML,
1061
- TEST_RUNTIME_TO_INJECT,
1067
+ testRuntimeToInject(config.port, config),
1062
1068
  "/tests.js"
1063
1069
  );
1064
1070
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1215,11 +1221,6 @@ function testRuntimeToInject(port, config) {
1215
1221
  window.socket.send(JSON.stringify({ event: 'connection' }));
1216
1222
  }
1217
1223
  });
1218
- window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
1219
- if (window.IS_PLAYWRIGHT) {
1220
- window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
1221
- }
1222
- });
1223
1224
  window.QUnit.on('testStart', (details) => {
1224
1225
  window.QUNIT_RESULT.totalTests++;
1225
1226
  window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
@@ -1239,7 +1240,7 @@ function testRuntimeToInject(port, config) {
1239
1240
  });
1240
1241
  window.QUnit.done((details) => {
1241
1242
  if (window.IS_PLAYWRIGHT) {
1242
- window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
1243
+ window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
1243
1244
  // Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
1244
1245
  // canonical completion signal for Playwright runs. waitForFunction is reserved
1245
1246
  // for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
@@ -1537,16 +1538,25 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1537
1538
  await page.addInitScript(() => {
1538
1539
  window.IS_PLAYWRIGHT = true;
1539
1540
  });
1540
- page.on("console", async (msg) => {
1541
+ config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
1542
+ page.on("console", (msg) => {
1541
1543
  const type = msg.type();
1542
1544
  const alwaysShow = type === "warning" || type === "error";
1543
1545
  if (!alwaysShow && !config.debug) return;
1544
- try {
1545
- const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
1546
- console.log(...values);
1547
- } catch {
1548
- console.log(msg.text());
1549
- }
1546
+ const handler = (async () => {
1547
+ try {
1548
+ const values = await Promise.all(
1549
+ msg.args().map(
1550
+ (arg) => arg.jsonValue().catch(() => arg.evaluate((v) => JSON.stringify(v)).then(JSON.parse))
1551
+ )
1552
+ );
1553
+ console.log(...values);
1554
+ } catch {
1555
+ console.log(msg.text());
1556
+ }
1557
+ })();
1558
+ config._pendingConsoleHandlers.add(handler);
1559
+ handler.finally(() => config._pendingConsoleHandlers?.delete(handler));
1550
1560
  });
1551
1561
  page.on("pageerror", (error) => {
1552
1562
  console.error(error.toString());
@@ -1712,7 +1722,7 @@ async function buildTestBundle(config, cachedContent) {
1712
1722
  // Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
1713
1723
  // packages from any node_modules on the ancestor chain of cwd — the same lookup
1714
1724
  // order Node itself uses when resolving require() from process.cwd().
1715
- nodePaths: ancestorNodeModules(process.cwd()),
1725
+ nodePaths: ANCESTOR_NODE_MODULES,
1716
1726
  bundle: true,
1717
1727
  logLevel: "silent",
1718
1728
  outfile,
@@ -1817,6 +1827,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1817
1827
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
1818
1828
  }
1819
1829
  if (!config.watch) {
1830
+ await flushConsoleHandlers(config._pendingConsoleHandlers);
1820
1831
  await Promise.all([
1821
1832
  connections.server && connections.server.close(),
1822
1833
  connections.browser && connections.browser.close()
@@ -1856,7 +1867,7 @@ function buildFilteredTests(filteredTests, outputPath, config) {
1856
1867
  contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
1857
1868
  resolveDir: process.cwd()
1858
1869
  },
1859
- nodePaths: ancestorNodeModules(process.cwd()),
1870
+ nodePaths: ANCESTOR_NODE_MODULES,
1860
1871
  bundle: true,
1861
1872
  logLevel: "silent",
1862
1873
  outfile: outputPath,
@@ -1958,7 +1969,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1958
1969
  clearTimeout(timeoutHandle);
1959
1970
  timeoutHandle = setTimeout(resolveTestRace, startupMs);
1960
1971
  await testRaceResult;
1961
- QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
1972
+ QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
1962
1973
  } catch (error) {
1963
1974
  targetError = error;
1964
1975
  console.log(error);
@@ -1969,6 +1980,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1969
1980
  config._onTestsJsServed = null;
1970
1981
  config._resetTestTimeout = null;
1971
1982
  config._testRunDone = null;
1983
+ config._lastQUnitResult = null;
1972
1984
  }
1973
1985
  if (!QUNIT_RESULT) {
1974
1986
  if (targetError) console.log(targetError);
@@ -1976,7 +1988,12 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1976
1988
  console.log(`# TIMEOUT: ${wsReason}`);
1977
1989
  console.log("BROWSER: runtime error thrown during executing tests");
1978
1990
  console.error("BROWSER: runtime error thrown during executing tests");
1979
- await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
1991
+ await failOnNonWatchMode(
1992
+ config.watch,
1993
+ { server, browser },
1994
+ config._groupMode,
1995
+ config._pendingConsoleHandlers
1996
+ );
1980
1997
  } else if (QUNIT_RESULT.totalTests === 0) {
1981
1998
  return;
1982
1999
  } else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
@@ -1986,16 +2003,22 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1986
2003
  );
1987
2004
  console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
1988
2005
  console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
1989
- await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
2006
+ await failOnNonWatchMode(
2007
+ config.watch,
2008
+ { server, browser },
2009
+ config._groupMode,
2010
+ config._pendingConsoleHandlers
2011
+ );
1990
2012
  } else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
1991
2013
  config.COUNTER.failCount = QUNIT_RESULT.failedTests;
1992
2014
  }
1993
2015
  }
1994
- async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
2016
+ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
1995
2017
  if (!watchMode) {
1996
2018
  if (groupMode) {
1997
2019
  throw new Error("Browser test run failed");
1998
2020
  }
2021
+ await flushConsoleHandlers(pendingHandlers);
1999
2022
  await Promise.all([
2000
2023
  connections.server && connections.server.close(),
2001
2024
  connections.browser && connections.browser.close()
@@ -2004,7 +2027,12 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
2004
2027
  process.exit(1);
2005
2028
  }
2006
2029
  }
2007
- var BundleError, ancestorNodeModules;
2030
+ async function flushConsoleHandlers(handlers, deadline = Date.now() + 2e3) {
2031
+ if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
2032
+ await Promise.allSettled([...handlers]);
2033
+ return flushConsoleHandlers(handlers, deadline);
2034
+ }
2035
+ var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES;
2008
2036
  var init_tests_in_browser = __esm({
2009
2037
  "lib/commands/run/tests-in-browser.ts"() {
2010
2038
  init_color();
@@ -2023,6 +2051,7 @@ var init_tests_in_browser = __esm({
2023
2051
  ancestorNodeModules = (dir) => dir.split(path5.sep).map(
2024
2052
  (_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
2025
2053
  );
2054
+ ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
2026
2055
  }
2027
2056
  });
2028
2057
 
@@ -2209,7 +2238,7 @@ var CHANGE_DEDUPE_MS;
2209
2238
  var init_file_watcher = __esm({
2210
2239
  "lib/setup/file-watcher.ts"() {
2211
2240
  init_color();
2212
- CHANGE_DEDUPE_MS = 30;
2241
+ CHANGE_DEDUPE_MS = 10;
2213
2242
  }
2214
2243
  });
2215
2244
 
@@ -2393,7 +2422,7 @@ async function run(config) {
2393
2422
  } else {
2394
2423
  const allFiles = Object.keys(config.fsTree);
2395
2424
  const groupCount = Math.min(allFiles.length, availableParallelism());
2396
- const groups = splitIntoGroups(allFiles, groupCount);
2425
+ const groups = await splitIntoGroups(allFiles, groupCount);
2397
2426
  config.COUNTER = {
2398
2427
  testCount: 0,
2399
2428
  failCount: 0,
@@ -2462,6 +2491,7 @@ async function run(config) {
2462
2491
  try {
2463
2492
  await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
2464
2493
  } finally {
2494
+ await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
2465
2495
  await Promise.all([
2466
2496
  connections.server && connections.server.close(),
2467
2497
  connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
@@ -2567,10 +2597,24 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
2567
2597
  }
2568
2598
  return cachedContent;
2569
2599
  }
2570
- function splitIntoGroups(files, groupCount) {
2571
- const groups = Array.from({ length: groupCount }, () => []);
2572
- files.forEach((file, i) => groups[i % groupCount].push(file));
2573
- return groups.filter((group) => group.length > 0);
2600
+ async function splitIntoGroups(files, groupCount) {
2601
+ const withSizes = await Promise.all(
2602
+ files.map(
2603
+ (f) => fs12.stat(f).then(({ size }) => ({ f, size })).catch(() => ({ f, size: 0 }))
2604
+ )
2605
+ );
2606
+ return withSizes.sort((a, b) => b.size - a.size).reduce(
2607
+ (groups, { f, size }) => {
2608
+ const { idx } = groups.reduce(
2609
+ (min, { total }, i) => total < min.total ? { idx: i, total } : min,
2610
+ { idx: 0, total: groups[0].total }
2611
+ );
2612
+ groups[idx].files.push(f);
2613
+ groups[idx].total += size;
2614
+ return groups;
2615
+ },
2616
+ Array.from({ length: groupCount }, () => ({ files: [], total: 0 }))
2617
+ ).flatMap((g) => g.files.length > 0 ? [g.files] : []);
2574
2618
  }
2575
2619
  function logWatcherAndKeyboardShortcutInfo(config, _server) {
2576
2620
  const prefix = "Watching files...";
@@ -2619,7 +2663,7 @@ init_color();
2619
2663
  var package_default = {
2620
2664
  name: "qunitx-cli",
2621
2665
  type: "module",
2622
- version: "0.19.0",
2666
+ version: "0.19.1",
2623
2667
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2624
2668
  author: "Izel Nakri",
2625
2669
  license: "MIT",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.19.0",
4
+ "version": "0.19.1",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",