qunitx-cli 0.20.0 → 0.21.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.
- package/README.md +111 -0
- package/dist/cli.js +210 -148
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -227,6 +227,117 @@ Options:
|
|
|
227
227
|
--browser=<name> Browser engine: chromium (default), firefox, or webkit
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
+
## Timezone
|
|
231
|
+
|
|
232
|
+
The browser inherits the **OS system timezone** automatically — no Playwright `timezoneId` option is involved. The browser's `Intl.DateTimeFormat().resolvedOptions().timeZone` will match the timezone that Node.js itself reads from the OS.
|
|
233
|
+
|
|
234
|
+
### Setting a timezone for tests
|
|
235
|
+
|
|
236
|
+
| Platform | How Chrome resolves the timezone | Override |
|
|
237
|
+
|----------|----------------------------------|---------|
|
|
238
|
+
| **Linux** | glibc reads `TZ` env var first, then `/etc/localtime` | `TZ=America/New_York npx qunitx …` works |
|
|
239
|
+
| **macOS** | CoreFoundation reads the system timezone (ignores `TZ`) | Must set the system timezone: `sudo systemsetup -settimezone America/New_York` |
|
|
240
|
+
| **Windows** | Reads the registry timezone (ignores `TZ`) | Must set the system timezone: `tzutil /s "Eastern Standard Time"` |
|
|
241
|
+
|
|
242
|
+
On Linux, the `TZ` env var is the simplest way to run tests in a specific timezone:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
TZ=UTC npx qunitx test/**/*.ts
|
|
246
|
+
TZ=America/Los_Angeles npx qunitx test/**/*.ts
|
|
247
|
+
TZ=Europe/Berlin npx qunitx test/**/*.ts
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### CI pitfalls
|
|
251
|
+
|
|
252
|
+
GitHub Actions (and most CI providers) run with **UTC** by default on all platforms. This is usually what you want for reproducible test results. If your tests assert on specific local times or date formatting, be aware:
|
|
253
|
+
|
|
254
|
+
**Linux CI** — override with `TZ` in your workflow step:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
- run: npx qunitx test/**/*.ts
|
|
258
|
+
env:
|
|
259
|
+
TZ: America/New_York
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**macOS CI** — `TZ` does not affect Chrome. Set the system timezone before running tests:
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
- run: sudo systemsetup -settimezone America/New_York
|
|
266
|
+
- run: npx qunitx test/**/*.ts
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Windows CI** — same constraint, use `tzutil`:
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
- run: tzutil /s "Eastern Standard Time"
|
|
273
|
+
- run: npx qunitx test/**/*.ts
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
If your test suite does not assert on local times or timezone-sensitive date formatting, none of this matters — the default UTC CI timezone is fine.
|
|
277
|
+
|
|
278
|
+
### Mocking dates and times in tests
|
|
279
|
+
|
|
280
|
+
For most cases you do not need to touch system settings or env vars at all. `Date`, `Intl`, and timers are plain browser globals — mock them in a qunitx `before` / `beforeEach` hook just like any other value:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// test/some-test.ts
|
|
284
|
+
import { module, test } from 'qunitx';
|
|
285
|
+
|
|
286
|
+
module('Invoice formatting', (hooks) => {
|
|
287
|
+
let realDate;
|
|
288
|
+
|
|
289
|
+
hooks.before(() => {
|
|
290
|
+
realDate = globalThis.Date;
|
|
291
|
+
// Pin "now" to a fixed instant for the whole module
|
|
292
|
+
const FIXED = new realDate('2024-06-01T12:00:00Z');
|
|
293
|
+
globalThis.Date = class extends realDate {
|
|
294
|
+
constructor(...args) { super(args.length ? args : [FIXED]); }
|
|
295
|
+
static now() { return FIXED.getTime(); }
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
hooks.after(() => { globalThis.Date = realDate; });
|
|
300
|
+
|
|
301
|
+
test('formats the current date correctly', (assert) => {
|
|
302
|
+
assert.equal(new Date().toISOString().slice(0, 10), '2024-06-01');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For richer control over timers (`setTimeout`, `setInterval`, `requestAnimationFrame`, …) use a fake-timer library such as [Sinon.JS](https://sinonjs.org/releases/latest/fake-timers/):
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
import sinon from 'sinon';
|
|
311
|
+
|
|
312
|
+
module('Debounce logic', (hooks) => {
|
|
313
|
+
let clock;
|
|
314
|
+
|
|
315
|
+
hooks.before(() => { clock = sinon.useFakeTimers({ now: new Date('2024-06-01T00:00:00Z') }); });
|
|
316
|
+
hooks.after(() => { clock.restore(); });
|
|
317
|
+
|
|
318
|
+
test('fires after 300 ms', (assert) => {
|
|
319
|
+
// clock.tick(300) advances fake time without waiting in real time
|
|
320
|
+
clock.tick(300);
|
|
321
|
+
assert.ok(/* your assertion */);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
If you need the mock active across the entire test run rather than inside a single module, put it in a `--before` script:
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
// scripts/mock-date.js (passed as: qunitx … --before=scripts/mock-date.js)
|
|
330
|
+
const realDate = globalThis.Date;
|
|
331
|
+
const FIXED = new realDate('2024-06-01T12:00:00Z');
|
|
332
|
+
|
|
333
|
+
globalThis.Date = class extends realDate {
|
|
334
|
+
constructor(...args) { super(args.length ? args : [FIXED]); }
|
|
335
|
+
static now() { return FIXED.getTime(); }
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
This runs in the browser context before any test module loads, so every test in the run sees the mocked `Date` with no changes to the OS, no env vars, and no qunitx-cli configuration.
|
|
340
|
+
|
|
230
341
|
## Development
|
|
231
342
|
|
|
232
343
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -38,10 +38,11 @@ var init_find_chrome = __esm({
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// lib/utils/kill-process-group.ts
|
|
41
|
+
import { spawnSync } from "node:child_process";
|
|
41
42
|
function killProcessGroup(pid) {
|
|
42
43
|
try {
|
|
43
44
|
if (process.platform === "win32") {
|
|
44
|
-
|
|
45
|
+
spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
|
45
46
|
} else {
|
|
46
47
|
process.kill(-pid, "SIGKILL");
|
|
47
48
|
}
|
|
@@ -199,8 +200,11 @@ var init_chromium_args = __esm({
|
|
|
199
200
|
// ── Sandbox / rendering ──────────────────────────────────────────────────────
|
|
200
201
|
"--no-sandbox",
|
|
201
202
|
// required in most CI/container environments
|
|
202
|
-
|
|
203
|
-
//
|
|
203
|
+
// SwiftShader software WebGL: needed on Linux CI (containerised, no GPU).
|
|
204
|
+
// Omit on macOS — Chrome uses Metal natively; SwiftShader crashes the renderer
|
|
205
|
+
// process on macOS arm64, causing "Target page, context or browser has been closed".
|
|
206
|
+
// Omit on Windows — ANGLE/D3D11 is available and SwiftShader is not needed.
|
|
207
|
+
...process.platform === "linux" ? ["--enable-unsafe-swiftshader"] : [],
|
|
204
208
|
// ── Window / UI ──────────────────────────────────────────────────────────────
|
|
205
209
|
"--window-size=1440,900",
|
|
206
210
|
"--hide-scrollbars",
|
|
@@ -307,7 +311,13 @@ var init_chrome_prelaunch = __esm({
|
|
|
307
311
|
else if (arg === "--watch" || arg === "-w") flags.watchFromArgv = true;
|
|
308
312
|
return flags;
|
|
309
313
|
},
|
|
310
|
-
|
|
314
|
+
// QUNITX_BROWSER env var seeds the default so prelaunch is skipped for firefox/webkit
|
|
315
|
+
// even when --browser is not passed on the command line (e.g. browser-compat CI).
|
|
316
|
+
{
|
|
317
|
+
browserFromArgv: process.env.QUNITX_BROWSER || "chromium",
|
|
318
|
+
openFromArgv: false,
|
|
319
|
+
watchFromArgv: false
|
|
320
|
+
}
|
|
311
321
|
));
|
|
312
322
|
openWatchMode = openFromArgv && watchFromArgv;
|
|
313
323
|
earlyChrome = null;
|
|
@@ -318,7 +328,7 @@ var init_chrome_prelaunch = __esm({
|
|
|
318
328
|
});
|
|
319
329
|
}
|
|
320
330
|
perfLog("chrome-prelaunch.ts: module evaluated");
|
|
321
|
-
prelaunchPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
|
|
331
|
+
prelaunchPromise = isRunCommand && browserFromArgv === "chromium" && process.platform !== "darwin" ? findChrome().then((chromePath) => {
|
|
322
332
|
perfLog("chrome-prelaunch.ts: findChrome resolved", chromePath);
|
|
323
333
|
return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
|
|
324
334
|
}).then((info) => {
|
|
@@ -922,8 +932,8 @@ var init_http = __esm({
|
|
|
922
932
|
});
|
|
923
933
|
}
|
|
924
934
|
/** Registers a GET route handler. */
|
|
925
|
-
get(
|
|
926
|
-
this.#registerRouteHandler("GET",
|
|
935
|
+
get(path10, handler) {
|
|
936
|
+
this.#registerRouteHandler("GET", path10, handler);
|
|
927
937
|
}
|
|
928
938
|
/**
|
|
929
939
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -954,32 +964,32 @@ var init_http = __esm({
|
|
|
954
964
|
});
|
|
955
965
|
}
|
|
956
966
|
/** Registers a POST route handler. */
|
|
957
|
-
post(
|
|
958
|
-
this.#registerRouteHandler("POST",
|
|
967
|
+
post(path10, handler) {
|
|
968
|
+
this.#registerRouteHandler("POST", path10, handler);
|
|
959
969
|
}
|
|
960
970
|
/** Registers a DELETE route handler. */
|
|
961
|
-
delete(
|
|
962
|
-
this.#registerRouteHandler("DELETE",
|
|
971
|
+
delete(path10, handler) {
|
|
972
|
+
this.#registerRouteHandler("DELETE", path10, handler);
|
|
963
973
|
}
|
|
964
974
|
/** Registers a PUT route handler. */
|
|
965
|
-
put(
|
|
966
|
-
this.#registerRouteHandler("PUT",
|
|
975
|
+
put(path10, handler) {
|
|
976
|
+
this.#registerRouteHandler("PUT", path10, handler);
|
|
967
977
|
}
|
|
968
978
|
/** Adds a middleware function to the chain. */
|
|
969
979
|
use(middleware) {
|
|
970
980
|
this.middleware.push(middleware);
|
|
971
981
|
}
|
|
972
|
-
#registerRouteHandler(method,
|
|
982
|
+
#registerRouteHandler(method, path10, handler) {
|
|
973
983
|
if (!this.routes[method]) {
|
|
974
984
|
this.routes[method] = {};
|
|
975
985
|
}
|
|
976
|
-
const paramNames = this.#extractParamNames(
|
|
977
|
-
this.routes[method][
|
|
978
|
-
path:
|
|
986
|
+
const paramNames = this.#extractParamNames(path10);
|
|
987
|
+
this.routes[method][path10] = {
|
|
988
|
+
path: path10,
|
|
979
989
|
handler,
|
|
980
990
|
paramNames,
|
|
981
|
-
isWildcard:
|
|
982
|
-
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(
|
|
991
|
+
isWildcard: path10 === "/*",
|
|
992
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path10, paramNames)}$`) : null
|
|
983
993
|
};
|
|
984
994
|
}
|
|
985
995
|
#handleRequest(req, res) {
|
|
@@ -1017,11 +1027,11 @@ var init_http = __esm({
|
|
|
1017
1027
|
return null;
|
|
1018
1028
|
}
|
|
1019
1029
|
return routes[url] || Object.values(routes).find((route) => {
|
|
1020
|
-
const { path:
|
|
1021
|
-
if (!isWildcard && !
|
|
1030
|
+
const { path: path10, isWildcard } = route;
|
|
1031
|
+
if (!isWildcard && !path10.includes(":")) {
|
|
1022
1032
|
return false;
|
|
1023
1033
|
}
|
|
1024
|
-
if (isWildcard || this.#matchPathSegments(
|
|
1034
|
+
if (isWildcard || this.#matchPathSegments(path10, url)) {
|
|
1025
1035
|
if (route.compiledRegex) {
|
|
1026
1036
|
const regexMatches = route.compiledRegex.exec(url);
|
|
1027
1037
|
if (regexMatches) {
|
|
@@ -1033,8 +1043,8 @@ var init_http = __esm({
|
|
|
1033
1043
|
return false;
|
|
1034
1044
|
}) || null;
|
|
1035
1045
|
}
|
|
1036
|
-
#matchPathSegments(
|
|
1037
|
-
const pathSegments =
|
|
1046
|
+
#matchPathSegments(path10, url) {
|
|
1047
|
+
const pathSegments = path10.split("/");
|
|
1038
1048
|
const urlSegments = url.split("/");
|
|
1039
1049
|
if (pathSegments.length !== urlSegments.length) {
|
|
1040
1050
|
return false;
|
|
@@ -1051,14 +1061,14 @@ var init_http = __esm({
|
|
|
1051
1061
|
}
|
|
1052
1062
|
return true;
|
|
1053
1063
|
}
|
|
1054
|
-
#buildRegexPattern(
|
|
1055
|
-
let regexPattern =
|
|
1064
|
+
#buildRegexPattern(path10, _paramNames) {
|
|
1065
|
+
let regexPattern = path10.replace(/:[^/]+/g, "([^/]+)");
|
|
1056
1066
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
1057
1067
|
return regexPattern;
|
|
1058
1068
|
}
|
|
1059
|
-
#extractParamNames(
|
|
1069
|
+
#extractParamNames(path10) {
|
|
1060
1070
|
const paramRegex = /:(\w+)/g;
|
|
1061
|
-
const paramMatches =
|
|
1071
|
+
const paramMatches = path10.match(paramRegex);
|
|
1062
1072
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
1063
1073
|
}
|
|
1064
1074
|
#extractParams(route, _url) {
|
|
@@ -1075,9 +1085,9 @@ var init_http = __esm({
|
|
|
1075
1085
|
|
|
1076
1086
|
// lib/setup/web-server.ts
|
|
1077
1087
|
import fs8 from "node:fs";
|
|
1078
|
-
import
|
|
1088
|
+
import path5 from "node:path";
|
|
1079
1089
|
function setupWebServer(config, cachedContent) {
|
|
1080
|
-
const STATIC_FILES_PATH =
|
|
1090
|
+
const STATIC_FILES_PATH = path5.resolve(config.projectRoot, config.output);
|
|
1081
1091
|
const server = new HTTPServer();
|
|
1082
1092
|
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
1083
1093
|
cachedContent.mainHTML.html,
|
|
@@ -1214,7 +1224,10 @@ function setupWebServer(config, cachedContent) {
|
|
|
1214
1224
|
config._testRunDone?.();
|
|
1215
1225
|
config._testRunDone = null;
|
|
1216
1226
|
}
|
|
1217
|
-
return saveHTML(
|
|
1227
|
+
return saveHTML(
|
|
1228
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
|
|
1229
|
+
htmlContent
|
|
1230
|
+
);
|
|
1218
1231
|
}
|
|
1219
1232
|
if (cachedContent._noTestsWarning) {
|
|
1220
1233
|
res.writeHead(200, HTML_HEADERS);
|
|
@@ -1222,14 +1235,20 @@ function setupWebServer(config, cachedContent) {
|
|
|
1222
1235
|
}
|
|
1223
1236
|
res.writeHead(200, HTML_HEADERS);
|
|
1224
1237
|
res.end(mainIndexHTML);
|
|
1225
|
-
saveHTML(
|
|
1238
|
+
saveHTML(
|
|
1239
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
|
|
1240
|
+
mainIndexHTML
|
|
1241
|
+
);
|
|
1226
1242
|
});
|
|
1227
1243
|
server.get("/qunitx.html", (_req, res) => {
|
|
1228
1244
|
if (cachedContent._buildError) {
|
|
1229
1245
|
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
1230
1246
|
res.writeHead(200, HTML_HEADERS);
|
|
1231
1247
|
res.end(htmlContent);
|
|
1232
|
-
return saveHTML(
|
|
1248
|
+
return saveHTML(
|
|
1249
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1250
|
+
htmlContent
|
|
1251
|
+
);
|
|
1233
1252
|
}
|
|
1234
1253
|
if (cachedContent._noTestsWarning) {
|
|
1235
1254
|
res.writeHead(200, HTML_HEADERS);
|
|
@@ -1237,7 +1256,10 @@ function setupWebServer(config, cachedContent) {
|
|
|
1237
1256
|
}
|
|
1238
1257
|
res.writeHead(200, HTML_HEADERS);
|
|
1239
1258
|
res.end(mainQunitxHTML);
|
|
1240
|
-
saveHTML(
|
|
1259
|
+
saveHTML(
|
|
1260
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1261
|
+
mainQunitxHTML
|
|
1262
|
+
);
|
|
1241
1263
|
});
|
|
1242
1264
|
server.get("/*", (req, res) => {
|
|
1243
1265
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
@@ -1249,13 +1271,13 @@ function setupWebServer(config, cachedContent) {
|
|
|
1249
1271
|
);
|
|
1250
1272
|
res.writeHead(200, HTML_HEADERS);
|
|
1251
1273
|
res.end(htmlContent);
|
|
1252
|
-
saveHTML(
|
|
1274
|
+
saveHTML(path5.join(path5.resolve(config.projectRoot, config.output), req.path), htmlContent);
|
|
1253
1275
|
return;
|
|
1254
1276
|
}
|
|
1255
1277
|
const url = req.url;
|
|
1256
1278
|
const requestStartedAt = Date.now();
|
|
1257
1279
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1258
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1280
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1259
1281
|
const stream = fs8.createReadStream(filePath);
|
|
1260
1282
|
stream.on("open", () => {
|
|
1261
1283
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -1280,7 +1302,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1280
1302
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
1281
1303
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
1282
1304
|
return assetPaths.reduce((result, assetPath) => {
|
|
1283
|
-
const normalizedFullAbsolutePath =
|
|
1305
|
+
const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
|
|
1284
1306
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1285
1307
|
}, html);
|
|
1286
1308
|
}
|
|
@@ -1288,27 +1310,23 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1288
1310
|
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1289
1311
|
return `<script>
|
|
1290
1312
|
(function() {
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
// tests.js (loaded as an async external script) dispatches this event after registering
|
|
1303
|
-
// all test modules. Decoupled from WS so Chrome can compile tests.js in a background
|
|
1304
|
-
// thread while the main thread handles the WebSocket handshake.
|
|
1305
|
-
window.addEventListener('qunitx:tests-ready', function() {
|
|
1306
|
-
testsLoaded = true;
|
|
1307
|
-
maybeStart();
|
|
1313
|
+
// setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
|
|
1314
|
+
// Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
|
|
1315
|
+
// so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
|
|
1316
|
+
let resolveWsReady = () => {};
|
|
1317
|
+
const wsReadyPromise = window.location.protocol === 'file:'
|
|
1318
|
+
? Promise.resolve()
|
|
1319
|
+
: new Promise(resolve => { resolveWsReady = resolve; });
|
|
1320
|
+
|
|
1321
|
+
// { once: true } auto-removes the listener after the first fire.
|
|
1322
|
+
const testsReadyPromise = new Promise(resolve => {
|
|
1323
|
+
window.addEventListener('qunitx:tests-ready', resolve, { once: true });
|
|
1308
1324
|
});
|
|
1309
1325
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1326
|
+
Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
|
|
1327
|
+
|
|
1328
|
+
// For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
|
|
1329
|
+
// is already resolved above, so setupQUnit fires as soon as tests load.
|
|
1312
1330
|
if (window.location.protocol === 'file:') return;
|
|
1313
1331
|
|
|
1314
1332
|
let wsRetryCount = 0;
|
|
@@ -1324,14 +1342,13 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1324
1342
|
}
|
|
1325
1343
|
|
|
1326
1344
|
window.socket.addEventListener('open', function() {
|
|
1327
|
-
|
|
1345
|
+
resolveWsReady();
|
|
1328
1346
|
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1329
1347
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1330
1348
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1331
1349
|
if (navigator.webdriver) {
|
|
1332
1350
|
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1333
1351
|
}
|
|
1334
|
-
maybeStart();
|
|
1335
1352
|
});
|
|
1336
1353
|
window.socket.addEventListener('error', function() {
|
|
1337
1354
|
retryOrFail();
|
|
@@ -1661,7 +1678,10 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1661
1678
|
}
|
|
1662
1679
|
res.writeHead(200, HTML_HEADERS);
|
|
1663
1680
|
res.end(mainGroupHTML);
|
|
1664
|
-
saveHTML(
|
|
1681
|
+
saveHTML(
|
|
1682
|
+
path5.join(path5.resolve(groupConfig.projectRoot, groupConfig.output), "index.html"),
|
|
1683
|
+
mainGroupHTML
|
|
1684
|
+
);
|
|
1665
1685
|
});
|
|
1666
1686
|
server.get(`/group-${groupId}/tests.js`, (_req, res) => {
|
|
1667
1687
|
const bytes = groupCachedContent.allTestCode?.length ?? null;
|
|
@@ -1755,10 +1775,10 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1755
1775
|
res.end("Not found");
|
|
1756
1776
|
return;
|
|
1757
1777
|
}
|
|
1758
|
-
const STATIC_FILES_PATH =
|
|
1778
|
+
const STATIC_FILES_PATH = path5.resolve(groupConfig.projectRoot, groupConfig.output);
|
|
1759
1779
|
const subPath = match[2] || "/";
|
|
1760
1780
|
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
1761
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1781
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1762
1782
|
const stream = fs8.createReadStream(filePath);
|
|
1763
1783
|
stream.on("open", () => {
|
|
1764
1784
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -1828,13 +1848,23 @@ async function launchBrowser(config) {
|
|
|
1828
1848
|
);
|
|
1829
1849
|
if (prelaunch) {
|
|
1830
1850
|
const connectStart = Date.now();
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1851
|
+
try {
|
|
1852
|
+
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
1853
|
+
endpointURL: prelaunch.cdpEndpoint,
|
|
1854
|
+
// Short timeout: if Chrome isn't CDP-ready within 5s (e.g. resource contention on
|
|
1855
|
+
// slow CI runners with many concurrent pre-launches), fall back to chromium.launch().
|
|
1856
|
+
timeout: 5e3
|
|
1857
|
+
});
|
|
1858
|
+
perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
|
|
1859
|
+
return browser;
|
|
1860
|
+
} catch {
|
|
1861
|
+
perfLog(
|
|
1862
|
+
`browser.js: connectOverCDP failed after ${Date.now() - connectStart}ms \u2014 falling back to chromium.launch()`
|
|
1863
|
+
);
|
|
1864
|
+
await shutdownPrelaunch();
|
|
1865
|
+
}
|
|
1836
1866
|
}
|
|
1837
|
-
const executablePath = await findChrome();
|
|
1867
|
+
const executablePath = process.platform !== "darwin" ? await findChrome() : null;
|
|
1838
1868
|
const launchOptions = {
|
|
1839
1869
|
args: CHROMIUM_ARGS,
|
|
1840
1870
|
headless: true,
|
|
@@ -1930,9 +1960,11 @@ var init_browser = __esm({
|
|
|
1930
1960
|
|
|
1931
1961
|
// lib/utils/open-output-in-browser.ts
|
|
1932
1962
|
import { spawn as spawn2 } from "node:child_process";
|
|
1963
|
+
import path6 from "node:path";
|
|
1964
|
+
import { pathToFileURL } from "node:url";
|
|
1933
1965
|
async function openOutputInBrowser(config) {
|
|
1934
1966
|
try {
|
|
1935
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` :
|
|
1967
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL(path6.join(path6.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
1936
1968
|
if (typeof config.open === "string") {
|
|
1937
1969
|
spawnDetached(config.open, [outputFile]);
|
|
1938
1970
|
return;
|
|
@@ -1978,9 +2010,10 @@ var init_time_counter = __esm({
|
|
|
1978
2010
|
});
|
|
1979
2011
|
|
|
1980
2012
|
// lib/utils/run-user-module.ts
|
|
2013
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1981
2014
|
async function runUserModule(modulePath, params, scriptPosition) {
|
|
1982
2015
|
try {
|
|
1983
|
-
const func = await import(modulePath);
|
|
2016
|
+
const func = await import(pathToFileURL2(modulePath).href);
|
|
1984
2017
|
if (func) {
|
|
1985
2018
|
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
1986
2019
|
}
|
|
@@ -2023,8 +2056,14 @@ var init_display_final_result = __esm({
|
|
|
2023
2056
|
|
|
2024
2057
|
// lib/commands/run/tests-in-browser.ts
|
|
2025
2058
|
import fs9 from "node:fs/promises";
|
|
2026
|
-
import
|
|
2059
|
+
import path7 from "node:path";
|
|
2027
2060
|
import esbuild from "esbuild";
|
|
2061
|
+
function toEsbuildImportPath(filePath) {
|
|
2062
|
+
const rel = path7.relative(process.cwd(), filePath);
|
|
2063
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
2064
|
+
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2065
|
+
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2066
|
+
}
|
|
2028
2067
|
function deriveBuildErrorType(error) {
|
|
2029
2068
|
const msgs = error?.errors ?? [];
|
|
2030
2069
|
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
@@ -2060,13 +2099,14 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2060
2099
|
console.log("# [buildTestBundle] fsTree is empty \u2014 skipping build (no test files found)");
|
|
2061
2100
|
return;
|
|
2062
2101
|
}
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2102
|
+
const outDir = path7.resolve(projectRoot, output);
|
|
2103
|
+
const outfile = path7.join(outDir, "tests.js");
|
|
2104
|
+
await fs9.mkdir(outDir, { recursive: true });
|
|
2065
2105
|
const sourcemap = "inline";
|
|
2066
2106
|
const needsDisk = true;
|
|
2067
2107
|
const buildOptions = {
|
|
2068
2108
|
stdin: {
|
|
2069
|
-
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
2109
|
+
contents: allTestFilePaths.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
2070
2110
|
resolveDir: process.cwd()
|
|
2071
2111
|
},
|
|
2072
2112
|
// Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
|
|
@@ -2093,30 +2133,28 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2093
2133
|
config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
2094
2134
|
Promise.all(
|
|
2095
2135
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
2096
|
-
const targetPath =
|
|
2136
|
+
const targetPath = path7.join(outDir, htmlPath);
|
|
2097
2137
|
if (htmlPath !== "/") {
|
|
2098
2138
|
await fs9.rm(targetPath, { force: true, recursive: true });
|
|
2099
|
-
await fs9.mkdir(
|
|
2139
|
+
await fs9.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
2100
2140
|
}
|
|
2101
2141
|
})
|
|
2102
2142
|
)
|
|
2103
2143
|
]);
|
|
2104
2144
|
cachedContent.allTestCode = allTestCode;
|
|
2105
|
-
config._sourceMapDecoder = extractInlineSourceMap(allTestCode,
|
|
2145
|
+
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, outDir);
|
|
2106
2146
|
} catch (error) {
|
|
2107
2147
|
cachedContent._buildError = {
|
|
2108
2148
|
type: deriveBuildErrorType(error),
|
|
2109
2149
|
formatted: formatBuildErrors(error)
|
|
2110
2150
|
};
|
|
2111
|
-
await fs9.writeFile(
|
|
2112
|
-
`${projectRoot}/${output}/index.html`,
|
|
2113
|
-
buildErrorHTML(cachedContent._buildError)
|
|
2114
|
-
);
|
|
2151
|
+
await fs9.writeFile(path7.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
2115
2152
|
throw error;
|
|
2116
2153
|
}
|
|
2117
2154
|
}
|
|
2118
2155
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
2119
2156
|
const { projectRoot, output } = config;
|
|
2157
|
+
const outDir = path7.resolve(projectRoot, output);
|
|
2120
2158
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
2121
2159
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
2122
2160
|
if (!config._groupMode) {
|
|
@@ -2144,16 +2182,13 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2144
2182
|
return connections;
|
|
2145
2183
|
}
|
|
2146
2184
|
if (runHasFilter) {
|
|
2147
|
-
const outputPath =
|
|
2185
|
+
const outputPath = path7.join(outDir, "filtered-tests.js");
|
|
2148
2186
|
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
2149
2187
|
targetTestFilesToFilter,
|
|
2150
2188
|
outputPath,
|
|
2151
2189
|
config
|
|
2152
2190
|
);
|
|
2153
|
-
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2154
|
-
cachedContent.filteredTestCode,
|
|
2155
|
-
`${projectRoot}/${output}`
|
|
2156
|
-
);
|
|
2191
|
+
config._sourceMapDecoder = extractInlineSourceMap(cachedContent.filteredTestCode, outDir);
|
|
2157
2192
|
}
|
|
2158
2193
|
const TIME_COUNTER = timeCounter();
|
|
2159
2194
|
if (runHasFilter) {
|
|
@@ -2187,7 +2222,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2187
2222
|
console.log(
|
|
2188
2223
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
2189
2224
|
);
|
|
2190
|
-
fs9.writeFile(
|
|
2225
|
+
fs9.writeFile(path7.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
2191
2226
|
() => {
|
|
2192
2227
|
}
|
|
2193
2228
|
);
|
|
@@ -2216,7 +2251,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2216
2251
|
formatted: formatBuildErrors(error)
|
|
2217
2252
|
};
|
|
2218
2253
|
fs9.writeFile(
|
|
2219
|
-
|
|
2254
|
+
path7.join(outDir, "qunitx.html"),
|
|
2220
2255
|
buildErrorHTML(cachedContent._buildError)
|
|
2221
2256
|
).catch(
|
|
2222
2257
|
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
@@ -2237,7 +2272,7 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
2237
2272
|
return buildWithOverlayfsRetry(
|
|
2238
2273
|
{
|
|
2239
2274
|
stdin: {
|
|
2240
|
-
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
2275
|
+
contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
|
|
2241
2276
|
resolveDir: process.cwd()
|
|
2242
2277
|
},
|
|
2243
2278
|
nodePaths: ANCESTOR_NODE_MODULES,
|
|
@@ -2428,7 +2463,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2428
2463
|
);
|
|
2429
2464
|
await Promise.all(
|
|
2430
2465
|
activeGroups.map(
|
|
2431
|
-
(group) => fs9.mkdir(
|
|
2466
|
+
(group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2432
2467
|
)
|
|
2433
2468
|
);
|
|
2434
2469
|
const sourcemap = "inline";
|
|
@@ -2442,13 +2477,13 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2442
2477
|
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2443
2478
|
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2444
2479
|
return {
|
|
2445
|
-
contents: activeGroups[slotIndex].files.map((filePath) => `import "${filePath}";`).join(""),
|
|
2480
|
+
contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
2446
2481
|
resolveDir: process.cwd()
|
|
2447
2482
|
};
|
|
2448
2483
|
});
|
|
2449
2484
|
}
|
|
2450
2485
|
};
|
|
2451
|
-
const esbuildOutdir =
|
|
2486
|
+
const esbuildOutdir = path7.join(projectRoot, "tmp");
|
|
2452
2487
|
const buildOptions = {
|
|
2453
2488
|
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2454
2489
|
in: `group-entry-${slotIndex}`,
|
|
@@ -2486,7 +2521,10 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2486
2521
|
const slotIndex = parseInt(match[1]);
|
|
2487
2522
|
const isMap = Boolean(match[2]);
|
|
2488
2523
|
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2489
|
-
const destPath =
|
|
2524
|
+
const destPath = path7.join(
|
|
2525
|
+
path7.resolve(config.projectRoot, config.output),
|
|
2526
|
+
"tests.js" + (isMap ? ".map" : "")
|
|
2527
|
+
);
|
|
2490
2528
|
if (!isMap) {
|
|
2491
2529
|
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2492
2530
|
config._sourceMapDecoder = extractInlineSourceMap(
|
|
@@ -2503,7 +2541,10 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2503
2541
|
await Promise.all(
|
|
2504
2542
|
activeGroups.map((group) => {
|
|
2505
2543
|
group.cachedContent._buildError = buildError;
|
|
2506
|
-
return fs9.writeFile(
|
|
2544
|
+
return fs9.writeFile(
|
|
2545
|
+
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2546
|
+
errorHtml
|
|
2547
|
+
).catch(
|
|
2507
2548
|
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2508
2549
|
`)
|
|
2509
2550
|
);
|
|
@@ -2529,8 +2570,8 @@ var init_tests_in_browser = __esm({
|
|
|
2529
2570
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2530
2571
|
}
|
|
2531
2572
|
};
|
|
2532
|
-
ancestorNodeModules = (dir) => dir.split(
|
|
2533
|
-
(_, i, parts) =>
|
|
2573
|
+
ancestorNodeModules = (dir) => dir.split(path7.sep).map(
|
|
2574
|
+
(_, i, parts) => path7.join(parts.slice(0, parts.length - i).join(path7.sep) || path7.sep, "node_modules")
|
|
2534
2575
|
);
|
|
2535
2576
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
2536
2577
|
RETRY_DELAY_MS = 100;
|
|
@@ -2548,7 +2589,7 @@ var init_tests_in_browser = __esm({
|
|
|
2548
2589
|
// lib/setup/file-watcher.ts
|
|
2549
2590
|
import fs10 from "node:fs";
|
|
2550
2591
|
import { stat, lstat } from "node:fs/promises";
|
|
2551
|
-
import
|
|
2592
|
+
import path8 from "node:path";
|
|
2552
2593
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
2553
2594
|
const extensions = config.extensions || ["js", "ts"];
|
|
2554
2595
|
const readyPromises = [];
|
|
@@ -2557,13 +2598,17 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2557
2598
|
const symlinkPollers = /* @__PURE__ */ new Map();
|
|
2558
2599
|
function trackSymlink(filePath) {
|
|
2559
2600
|
if (symlinkPollers.has(filePath)) return;
|
|
2560
|
-
const handler = (curr) => {
|
|
2601
|
+
const handler = (curr, prev) => {
|
|
2561
2602
|
if (curr.nlink === 0) {
|
|
2562
2603
|
fs10.unwatchFile(filePath, handler);
|
|
2563
2604
|
symlinkPollers.delete(filePath);
|
|
2564
2605
|
if (filePath in config.fsTree) {
|
|
2565
2606
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
2566
2607
|
}
|
|
2608
|
+
} else if ((process.platform === "win32" || process.platform === "darwin") && curr.mtimeMs !== prev.mtimeMs) {
|
|
2609
|
+
if (filePath in config.fsTree) {
|
|
2610
|
+
handleWatchEvent(config, extensions, "change", filePath, onEventFunc, onFinishFunc);
|
|
2611
|
+
}
|
|
2567
2612
|
}
|
|
2568
2613
|
};
|
|
2569
2614
|
fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
@@ -2575,22 +2620,23 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2575
2620
|
}
|
|
2576
2621
|
for (const watchPath of testFileLookupPaths) {
|
|
2577
2622
|
let ready = false;
|
|
2578
|
-
const
|
|
2623
|
+
const lastEventMs = {};
|
|
2624
|
+
const seenMtimeMs = {};
|
|
2579
2625
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2580
2626
|
if (!ready || !filename) return;
|
|
2581
|
-
const fullPath = filename ===
|
|
2627
|
+
const fullPath = filename === path8.basename(watchPath) ? watchPath : path8.join(watchPath, filename);
|
|
2582
2628
|
if (eventType === "change") {
|
|
2583
2629
|
const now = Date.now();
|
|
2584
|
-
const last =
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2630
|
+
const last = lastEventMs[fullPath] ?? 0;
|
|
2631
|
+
lastEventMs[fullPath] = now;
|
|
2632
|
+
try {
|
|
2633
|
+
const { mtimeMs } = await stat(fullPath);
|
|
2634
|
+
const prevMtime = seenMtimeMs[fullPath] ?? 0;
|
|
2635
|
+
seenMtimeMs[fullPath] = mtimeMs;
|
|
2636
|
+
if (now - last < CHANGE_DEDUPE_MS && mtimeMs > 0 && mtimeMs === prevMtime) return;
|
|
2637
|
+
if (config._lastBuildEndMs && mtimeMs < Math.floor(config._lastBuildEndMs / 1e3) * 1e3)
|
|
2638
|
+
return;
|
|
2639
|
+
} catch {
|
|
2594
2640
|
}
|
|
2595
2641
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
2596
2642
|
}
|
|
@@ -2607,8 +2653,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2607
2653
|
}
|
|
2608
2654
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
2609
2655
|
});
|
|
2610
|
-
const parentDir =
|
|
2611
|
-
const watchedBasename =
|
|
2656
|
+
const parentDir = path8.dirname(watchPath);
|
|
2657
|
+
const watchedBasename = path8.basename(watchPath);
|
|
2612
2658
|
let parentUnlinkFired = false;
|
|
2613
2659
|
const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
|
|
2614
2660
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
@@ -2664,14 +2710,16 @@ async function classifyRenameEvent(fullPath, fsTree) {
|
|
|
2664
2710
|
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2665
2711
|
try {
|
|
2666
2712
|
const statResult = await stat(fullPath);
|
|
2667
|
-
|
|
2713
|
+
if (statResult.isDirectory()) return "addDir";
|
|
2714
|
+
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2668
2715
|
} catch {
|
|
2669
2716
|
}
|
|
2670
2717
|
}
|
|
2671
2718
|
if (!fsTree) return null;
|
|
2672
2719
|
if (fullPath in fsTree) return "unlink";
|
|
2673
|
-
|
|
2674
|
-
|
|
2720
|
+
return Object.keys(fsTree).some(
|
|
2721
|
+
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2722
|
+
) ? "unlinkDir" : null;
|
|
2675
2723
|
}
|
|
2676
2724
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
2677
2725
|
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
@@ -2711,15 +2759,15 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
2711
2759
|
}
|
|
2712
2760
|
});
|
|
2713
2761
|
}
|
|
2714
|
-
function mutateFSTree(fsTree, event,
|
|
2762
|
+
function mutateFSTree(fsTree, event, filePath) {
|
|
2715
2763
|
if (event === "add") {
|
|
2716
|
-
fsTree[
|
|
2764
|
+
fsTree[filePath] = null;
|
|
2717
2765
|
} else if (event === "unlink") {
|
|
2718
|
-
delete fsTree[
|
|
2766
|
+
delete fsTree[filePath];
|
|
2719
2767
|
} else if (event === "unlinkDir") {
|
|
2720
|
-
const dirPrefix = path7.endsWith("/") ? path7 : path7 + "/";
|
|
2721
2768
|
for (const treePath of Object.keys(fsTree)) {
|
|
2722
|
-
if (treePath.startsWith(
|
|
2769
|
+
if (treePath.startsWith(filePath + "/") || treePath.startsWith(filePath + "\\"))
|
|
2770
|
+
delete fsTree[treePath];
|
|
2723
2771
|
}
|
|
2724
2772
|
}
|
|
2725
2773
|
}
|
|
@@ -2815,24 +2863,27 @@ var init_keyboard_events = __esm({
|
|
|
2815
2863
|
|
|
2816
2864
|
// lib/setup/write-output-static-files.ts
|
|
2817
2865
|
import fs11 from "node:fs/promises";
|
|
2866
|
+
import path9 from "node:path";
|
|
2818
2867
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
2819
2868
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
2820
|
-
const htmlRelativePath =
|
|
2821
|
-
|
|
2869
|
+
const htmlRelativePath = path9.relative(projectRoot, staticHTMLKey);
|
|
2870
|
+
const outDir = path9.resolve(projectRoot, output);
|
|
2871
|
+
await ensureFolderExists(path9.join(outDir, htmlRelativePath));
|
|
2822
2872
|
await fs11.writeFile(
|
|
2823
|
-
|
|
2873
|
+
path9.join(outDir, htmlRelativePath),
|
|
2824
2874
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
2825
2875
|
);
|
|
2826
2876
|
});
|
|
2827
2877
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
2828
|
-
const assetRelativePath =
|
|
2829
|
-
|
|
2830
|
-
await
|
|
2878
|
+
const assetRelativePath = path9.relative(projectRoot, assetAbsolutePath);
|
|
2879
|
+
const outDir = path9.resolve(projectRoot, output);
|
|
2880
|
+
await ensureFolderExists(path9.join(outDir, assetRelativePath));
|
|
2881
|
+
await fs11.copyFile(assetAbsolutePath, path9.join(outDir, assetRelativePath));
|
|
2831
2882
|
});
|
|
2832
2883
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
2833
2884
|
}
|
|
2834
2885
|
async function ensureFolderExists(assetPath) {
|
|
2835
|
-
await fs11.mkdir(
|
|
2886
|
+
await fs11.mkdir(path9.dirname(assetPath), { recursive: true });
|
|
2836
2887
|
}
|
|
2837
2888
|
var init_write_output_static_files = __esm({
|
|
2838
2889
|
"lib/setup/write-output-static-files.ts"() {
|
|
@@ -3062,7 +3113,7 @@ async function run(config) {
|
|
|
3062
3113
|
(err) => config.debug && process.stderr.write(`# [qunitx] persistTimings: ${err.message}
|
|
3063
3114
|
`)
|
|
3064
3115
|
);
|
|
3065
|
-
printFileTimings(fileTimes, config.projectRoot);
|
|
3116
|
+
if (config.debug) printFileTimings(fileTimes, config.projectRoot);
|
|
3066
3117
|
if (config.after) {
|
|
3067
3118
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
3068
3119
|
}
|
|
@@ -3248,7 +3299,7 @@ init_color();
|
|
|
3248
3299
|
var package_default = {
|
|
3249
3300
|
name: "qunitx-cli",
|
|
3250
3301
|
type: "module",
|
|
3251
|
-
version: "0.
|
|
3302
|
+
version: "0.21.0",
|
|
3252
3303
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
3253
3304
|
author: "Izel Nakri",
|
|
3254
3305
|
license: "MIT",
|
|
@@ -3304,9 +3355,9 @@ var package_default = {
|
|
|
3304
3355
|
},
|
|
3305
3356
|
devDependencies: {
|
|
3306
3357
|
"js-yaml": "^4.1.1",
|
|
3307
|
-
prettier: "^3.8.
|
|
3308
|
-
qunitx: "^1.2.
|
|
3309
|
-
typescript: "^6.0.
|
|
3358
|
+
prettier: "^3.8.3",
|
|
3359
|
+
qunitx: "^1.2.8",
|
|
3360
|
+
typescript: "^6.0.3"
|
|
3310
3361
|
},
|
|
3311
3362
|
volta: {
|
|
3312
3363
|
node: "24.14.0"
|
|
@@ -3364,9 +3415,9 @@ import process2 from "node:process";
|
|
|
3364
3415
|
|
|
3365
3416
|
// lib/utils/path-exists.ts
|
|
3366
3417
|
import fs2 from "node:fs/promises";
|
|
3367
|
-
async function pathExists(
|
|
3418
|
+
async function pathExists(path10) {
|
|
3368
3419
|
try {
|
|
3369
|
-
await fs2.access(
|
|
3420
|
+
await fs2.access(path10);
|
|
3370
3421
|
return true;
|
|
3371
3422
|
} catch {
|
|
3372
3423
|
return false;
|
|
@@ -3438,7 +3489,7 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
|
3438
3489
|
const targetDirectory = path2.dirname(targetPath);
|
|
3439
3490
|
const _targetOutputPath = path2.relative(
|
|
3440
3491
|
targetDirectory,
|
|
3441
|
-
|
|
3492
|
+
path2.join(path2.resolve(projectRoot, config.output), "tests.js")
|
|
3442
3493
|
);
|
|
3443
3494
|
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
3444
3495
|
"{{applicationName}}",
|
|
@@ -3484,17 +3535,17 @@ function pathToModuleName(filePath) {
|
|
|
3484
3535
|
async function generateTestFiles() {
|
|
3485
3536
|
const projectRoot = await findProjectRoot();
|
|
3486
3537
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
3487
|
-
const
|
|
3488
|
-
if (await pathExists(
|
|
3489
|
-
console.log(`${
|
|
3538
|
+
const path10 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
3539
|
+
if (await pathExists(path10)) {
|
|
3540
|
+
console.log(`${path10} already exists!`);
|
|
3490
3541
|
return;
|
|
3491
3542
|
}
|
|
3492
3543
|
const testJSContent = await readTemplate("test.js");
|
|
3493
|
-
const targetFolderPaths =
|
|
3544
|
+
const targetFolderPaths = path10.split("/");
|
|
3494
3545
|
targetFolderPaths.pop();
|
|
3495
3546
|
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
3496
|
-
await fs5.writeFile(
|
|
3497
|
-
console.log(green(`${
|
|
3547
|
+
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3548
|
+
console.log(green(`${path10} written`));
|
|
3498
3549
|
}
|
|
3499
3550
|
|
|
3500
3551
|
// lib/setup/config.ts
|
|
@@ -3595,23 +3646,24 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
3595
3646
|
});
|
|
3596
3647
|
return result.map((metaItem) => metaItem.input);
|
|
3597
3648
|
}
|
|
3598
|
-
function pathIsFile(
|
|
3599
|
-
const inputs2 =
|
|
3649
|
+
function pathIsFile(path10) {
|
|
3650
|
+
const inputs2 = path10.split("/");
|
|
3600
3651
|
return inputs2[inputs2.length - 1].includes(".");
|
|
3601
3652
|
}
|
|
3602
3653
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
3603
|
-
return paths.some((
|
|
3604
|
-
if (
|
|
3654
|
+
return paths.some((path10) => {
|
|
3655
|
+
if (path10 === targetPath) {
|
|
3605
3656
|
return false;
|
|
3606
3657
|
}
|
|
3607
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
3658
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path10));
|
|
3608
3659
|
});
|
|
3609
3660
|
}
|
|
3610
|
-
function buildGlobFormat(
|
|
3611
|
-
return
|
|
3661
|
+
function buildGlobFormat(path10) {
|
|
3662
|
+
return path10.isFile ? path10.input : `${path10.input}/**`;
|
|
3612
3663
|
}
|
|
3613
3664
|
|
|
3614
3665
|
// lib/utils/parse-cli-flags.ts
|
|
3666
|
+
import path4 from "node:path";
|
|
3615
3667
|
var FALLBACK_TIMEOUT_MS = 1e4;
|
|
3616
3668
|
function parseCliFlags(projectRoot) {
|
|
3617
3669
|
const providedFlags = process.argv.slice(2).reduce(
|
|
@@ -3664,12 +3716,22 @@ function parseCliFlags(projectRoot) {
|
|
|
3664
3716
|
return result;
|
|
3665
3717
|
}
|
|
3666
3718
|
result.inputs.add(
|
|
3667
|
-
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg :
|
|
3719
|
+
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path4.join(process.cwd(), arg)
|
|
3668
3720
|
);
|
|
3669
3721
|
return result;
|
|
3670
3722
|
},
|
|
3671
3723
|
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
3672
3724
|
);
|
|
3725
|
+
if (!providedFlags.browser && process.env.QUNITX_BROWSER) {
|
|
3726
|
+
const envBrowser = process.env.QUNITX_BROWSER;
|
|
3727
|
+
if (!["chromium", "firefox", "webkit"].includes(envBrowser)) {
|
|
3728
|
+
console.error(
|
|
3729
|
+
`Invalid QUNITX_BROWSER value: "${envBrowser}". Must be one of: chromium, firefox, webkit`
|
|
3730
|
+
);
|
|
3731
|
+
process.exit(1);
|
|
3732
|
+
}
|
|
3733
|
+
providedFlags.browser = envBrowser;
|
|
3734
|
+
}
|
|
3673
3735
|
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
3674
3736
|
}
|
|
3675
3737
|
function parseBoolean(result, defaultValue = true) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.21.0",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"author": "Izel Nakri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"js-yaml": "^4.1.1",
|
|
60
|
-
"prettier": "^3.8.
|
|
61
|
-
"qunitx": "^1.2.
|
|
62
|
-
"typescript": "^6.0.
|
|
60
|
+
"prettier": "^3.8.3",
|
|
61
|
+
"qunitx": "^1.2.8",
|
|
62
|
+
"typescript": "^6.0.3"
|
|
63
63
|
},
|
|
64
64
|
"volta": {
|
|
65
65
|
"node": "24.14.0"
|