qunitx-cli 0.10.0 → 0.11.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 +123 -33
  2. package/package.json +5 -4
package/dist/cli.js CHANGED
@@ -129,7 +129,13 @@ var init_early_chrome = __esm({
129
129
  openWatchMode = openFromArgv && watchFromArgv;
130
130
  earlyChromeProcRef = null;
131
131
  if (!openWatchMode) {
132
- process.on("exit", () => earlyChromeProcRef?.kill());
132
+ process.on("exit", () => {
133
+ if (!earlyChromeProcRef) return;
134
+ try {
135
+ earlyChromeProcRef.kill("SIGKILL");
136
+ } catch {
137
+ }
138
+ });
133
139
  }
134
140
  perfLog("early-chrome.js: module evaluated");
135
141
  earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
@@ -695,10 +701,34 @@ function setupWebServer(config, cachedContent) {
695
701
  });
696
702
  });
697
703
  server.get("/tests.js", (_req, res) => {
704
+ const bytes = cachedContent.allTestCode?.length ?? null;
705
+ console.log(
706
+ `# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}`
707
+ );
708
+ if (bytes === null) {
709
+ res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
710
+ res.end(
711
+ 'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
712
+ );
713
+ return;
714
+ }
715
+ config._onTestsJsServed?.();
698
716
  res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
699
717
  res.end(cachedContent.allTestCode);
700
718
  });
701
719
  server.get("/filtered-tests.js", (_req, res) => {
720
+ const bytes = cachedContent.filteredTestCode?.length ?? null;
721
+ console.log(
722
+ `# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}`
723
+ );
724
+ if (bytes === null) {
725
+ res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
726
+ res.end(
727
+ 'console.error("[qunitx] /filtered-tests.js requested before bundle was built \u2014 filteredTestCode is null");'
728
+ );
729
+ return;
730
+ }
731
+ config._onTestsJsServed?.();
702
732
  res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
703
733
  res.end(cachedContent.filteredTestCode);
704
734
  });
@@ -1002,7 +1032,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1002
1032
  window.IS_PLAYWRIGHT = true;
1003
1033
  });
1004
1034
  page.on("console", async (msg) => {
1005
- if (!config.debug) return;
1035
+ const type = msg.type();
1036
+ const alwaysShow = type === "warning" || type === "error";
1037
+ if (!alwaysShow && !config.debug) return;
1006
1038
  try {
1007
1039
  const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
1008
1040
  console.log(...values);
@@ -1121,23 +1153,31 @@ import esbuild from "esbuild";
1121
1153
  async function buildTestBundle(config, cachedContent) {
1122
1154
  const { projectRoot, output } = config;
1123
1155
  const allTestFilePaths = Object.keys(config.fsTree);
1156
+ if (allTestFilePaths.length === 0) {
1157
+ console.log("# [buildTestBundle] fsTree is empty \u2014 skipping build (no test files found)");
1158
+ return;
1159
+ }
1160
+ const outfile = `${projectRoot}/${output}/tests.js`;
1124
1161
  await Promise.all([
1125
- esbuild.build({
1126
- stdin: {
1127
- contents: allTestFilePaths.map((f) => `import "${f}";`).join(""),
1128
- resolveDir: process.cwd()
1162
+ buildWithOverlayfsRetry(
1163
+ {
1164
+ stdin: {
1165
+ contents: allTestFilePaths.map((f) => `import "${f}";`).join(""),
1166
+ resolveDir: process.cwd()
1167
+ },
1168
+ bundle: true,
1169
+ logLevel: "error",
1170
+ outfile,
1171
+ keepNames: true,
1172
+ sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
1173
+ // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1174
+ // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1175
+ // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1176
+ // all browsers and does not require changes to user test code.
1177
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1129
1178
  },
1130
- bundle: true,
1131
- logLevel: "error",
1132
- outfile: `${projectRoot}/${output}/tests.js`,
1133
- keepNames: true,
1134
- sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
1135
- // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1136
- // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1137
- // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1138
- // all browsers and does not require changes to user test code.
1139
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1140
- }),
1179
+ outfile
1180
+ ),
1141
1181
  Promise.all(
1142
1182
  cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
1143
1183
  const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
@@ -1148,7 +1188,7 @@ async function buildTestBundle(config, cachedContent) {
1148
1188
  })
1149
1189
  )
1150
1190
  ]);
1151
- cachedContent.allTestCode = await fs8.readFile(`${projectRoot}/${output}/tests.js`);
1191
+ cachedContent.allTestCode = await fs8.readFile(outfile);
1152
1192
  }
1153
1193
  async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
1154
1194
  const { projectRoot, output } = config;
@@ -1162,6 +1202,9 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1162
1202
  if (!cachedContent.allTestCode) {
1163
1203
  await buildTestBundle(config, cachedContent);
1164
1204
  }
1205
+ if (!cachedContent.allTestCode) {
1206
+ return connections;
1207
+ }
1165
1208
  if (runHasFilter) {
1166
1209
  const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
1167
1210
  await buildFilteredTests(targetTestFilesToFilter, outputPath, config);
@@ -1204,17 +1247,42 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1204
1247
  return connections;
1205
1248
  }
1206
1249
  function buildFilteredTests(filteredTests, outputPath, config) {
1207
- return esbuild.build({
1208
- stdin: {
1209
- contents: filteredTests.map((f) => `import "${f}";`).join(""),
1210
- resolveDir: process.cwd()
1250
+ return buildWithOverlayfsRetry(
1251
+ {
1252
+ stdin: {
1253
+ contents: filteredTests.map((f) => `import "${f}";`).join(""),
1254
+ resolveDir: process.cwd()
1255
+ },
1256
+ bundle: true,
1257
+ logLevel: "error",
1258
+ outfile: outputPath,
1259
+ sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
1260
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1211
1261
  },
1212
- bundle: true,
1213
- logLevel: "error",
1214
- outfile: outputPath,
1215
- sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
1216
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1217
- });
1262
+ outputPath
1263
+ );
1264
+ }
1265
+ async function buildWithOverlayfsRetry(options, outfile) {
1266
+ const RETRY_DELAY_MS = 100;
1267
+ const MAX_RETRIES = 3;
1268
+ const EMPTY_BUNDLE_THRESHOLD = 500;
1269
+ let result = await esbuild.build(options);
1270
+ for (let retry = 1; retry <= MAX_RETRIES; retry++) {
1271
+ const bytes2 = (await fs8.stat(outfile)).size;
1272
+ if (bytes2 >= EMPTY_BUNDLE_THRESHOLD) return result;
1273
+ console.log(
1274
+ `# [buildWithOverlayfsRetry] bundle is ${bytes2} bytes (< ${EMPTY_BUNDLE_THRESHOLD}) on attempt ${retry}/${MAX_RETRIES} \u2014 overlayfs flush race, retrying in ${RETRY_DELAY_MS}ms`
1275
+ );
1276
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
1277
+ result = await esbuild.build(options);
1278
+ }
1279
+ const bytes = (await fs8.stat(outfile)).size;
1280
+ if (bytes < EMPTY_BUNDLE_THRESHOLD) {
1281
+ console.log(
1282
+ `# [buildWithOverlayfsRetry] bundle is ${bytes} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
1283
+ );
1284
+ }
1285
+ return result;
1218
1286
  }
1219
1287
  async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
1220
1288
  let QUNIT_RESULT;
@@ -1233,6 +1301,10 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1233
1301
  clearTimeout(timeoutHandle);
1234
1302
  timeoutHandle = setTimeout(resolveTestRace, config.timeout * 3);
1235
1303
  };
1304
+ config._onTestsJsServed = () => {
1305
+ clearTimeout(timeoutHandle);
1306
+ timeoutHandle = setTimeout(resolveTestRace, config.timeout * 4);
1307
+ };
1236
1308
  config._resetTestTimeout = () => {
1237
1309
  wsConnected = true;
1238
1310
  clearTimeout(timeoutHandle);
@@ -1256,6 +1328,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
1256
1328
  } finally {
1257
1329
  clearTimeout(timeoutHandle);
1258
1330
  config._onWsOpen = null;
1331
+ config._onTestsJsServed = null;
1259
1332
  config._resetTestTimeout = null;
1260
1333
  config._testRunDone = null;
1261
1334
  }
@@ -1341,6 +1414,20 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1341
1414
  onFinishFunc
1342
1415
  );
1343
1416
  } catch {
1417
+ await new Promise((resolve) => setTimeout(resolve, 50));
1418
+ try {
1419
+ const s = await stat(fullPath);
1420
+ handleWatchEvent(
1421
+ config,
1422
+ extensions,
1423
+ s.isDirectory() ? "addDir" : "add",
1424
+ fullPath,
1425
+ onEventFunc,
1426
+ onFinishFunc
1427
+ );
1428
+ return;
1429
+ } catch {
1430
+ }
1344
1431
  if (!(config.fsTree && fullPath in config.fsTree)) return;
1345
1432
  handleWatchEvent(config, extensions, "unlink", fullPath, onEventFunc, onFinishFunc);
1346
1433
  }
@@ -1794,7 +1881,7 @@ init_color();
1794
1881
  var package_default = {
1795
1882
  name: "qunitx-cli",
1796
1883
  type: "module",
1797
- version: "0.10.0",
1884
+ version: "0.11.0",
1798
1885
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
1799
1886
  author: "Izel Nakri",
1800
1887
  license: "MIT",
@@ -1826,6 +1913,7 @@ var package_default = {
1826
1913
  postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
1827
1914
  test: `node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require("os").availableParallelism()') test/**/*-test.ts`,
1828
1915
  "test:browser": `node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require("os").availableParallelism()') test/flags/*-test.ts test/inputs/*-test.ts`,
1916
+ "test:release": "bash scripts/test-release.sh",
1829
1917
  "test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
1830
1918
  "test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
1831
1919
  },
@@ -1841,15 +1929,15 @@ var package_default = {
1841
1929
  url: "git+https://github.com/izelnakri/qunitx-cli.git"
1842
1930
  },
1843
1931
  dependencies: {
1844
- esbuild: "^0.27.3",
1845
- "playwright-core": "^1.58.2",
1932
+ esbuild: "^0.28.0",
1933
+ "playwright-core": "^1.59.1",
1846
1934
  ws: "^8.20.0"
1847
1935
  },
1848
1936
  devDependencies: {
1849
1937
  cors: "^2.8.6",
1850
1938
  express: "^5.2.1",
1851
1939
  "js-yaml": "^4.1.1",
1852
- prettier: "^3.8.1",
1940
+ prettier: "^3.8.2",
1853
1941
  qunitx: "^1.2.1",
1854
1942
  typescript: "^6.0.2"
1855
1943
  },
@@ -2214,7 +2302,9 @@ async function setupConfig() {
2214
2302
  lastRanTestFiles: null,
2215
2303
  COUNTER: { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 },
2216
2304
  _testRunDone: null,
2217
- _resetTestTimeout: null
2305
+ _resetTestTimeout: null,
2306
+ _onWsOpen: null,
2307
+ _onTestsJsServed: null
2218
2308
  };
2219
2309
  config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
2220
2310
  config.fsTree = await buildFSTree(config.testFileLookupPaths, config);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "postinstall": "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
34
34
  "test": "node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/**/*-test.ts",
35
35
  "test:browser": "node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/flags/*-test.ts test/inputs/*-test.ts",
36
+ "test:release": "bash scripts/test-release.sh",
36
37
  "test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
37
38
  "test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
38
39
  },
@@ -48,15 +49,15 @@
48
49
  "url": "git+https://github.com/izelnakri/qunitx-cli.git"
49
50
  },
50
51
  "dependencies": {
51
- "esbuild": "^0.27.3",
52
- "playwright-core": "^1.58.2",
52
+ "esbuild": "^0.28.0",
53
+ "playwright-core": "^1.59.1",
53
54
  "ws": "^8.20.0"
54
55
  },
55
56
  "devDependencies": {
56
57
  "cors": "^2.8.6",
57
58
  "express": "^5.2.1",
58
59
  "js-yaml": "^4.1.1",
59
- "prettier": "^3.8.1",
60
+ "prettier": "^3.8.2",
60
61
  "qunitx": "^1.2.1",
61
62
  "typescript": "^6.0.2"
62
63
  },