qunitx-cli 0.9.3 → 0.9.7
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/bin/qunitx.js +71 -0
- package/dist/cli.js +2009 -0
- package/package.json +16 -8
- package/cli.ts +0 -39
- package/deno.json +0 -18
- package/deno.lock +0 -648
- package/lib/commands/generate.ts +0 -33
- package/lib/commands/help.ts +0 -37
- package/lib/commands/init.ts +0 -77
- package/lib/commands/run/tests-in-browser.ts +0 -221
- package/lib/commands/run.ts +0 -279
- package/lib/servers/http.ts +0 -321
- package/lib/setup/bind-server-to-port.ts +0 -14
- package/lib/setup/browser.ts +0 -101
- package/lib/setup/config.ts +0 -55
- package/lib/setup/default-project-config-values.ts +0 -9
- package/lib/setup/file-watcher.ts +0 -134
- package/lib/setup/fs-tree.ts +0 -64
- package/lib/setup/keyboard-events.ts +0 -38
- package/lib/setup/test-file-paths.ts +0 -92
- package/lib/setup/web-server.ts +0 -274
- package/lib/setup/write-output-static-files.ts +0 -33
- package/lib/tap/display-final-result.ts +0 -25
- package/lib/tap/display-test-result.ts +0 -109
- package/lib/tap/dump-yaml.ts +0 -84
- package/lib/types.ts +0 -61
- package/lib/utils/chromium-args.ts +0 -18
- package/lib/utils/color.ts +0 -66
- package/lib/utils/early-chrome.ts +0 -39
- package/lib/utils/find-chrome.ts +0 -38
- package/lib/utils/find-internal-assets-from-html.ts +0 -18
- package/lib/utils/find-project-root.ts +0 -20
- package/lib/utils/indent-string.ts +0 -24
- package/lib/utils/listen-to-keyboard-key.ts +0 -57
- package/lib/utils/parse-cli-flags.ts +0 -95
- package/lib/utils/path-exists.ts +0 -21
- package/lib/utils/perf-logger.ts +0 -25
- package/lib/utils/pre-launch-chrome.ts +0 -45
- package/lib/utils/read-boilerplate.ts +0 -15
- package/lib/utils/resolve-port-number-for.ts +0 -29
- package/lib/utils/run-user-module.ts +0 -28
- package/lib/utils/search-in-parent-directories.ts +0 -25
- package/lib/utils/time-counter.ts +0 -19
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// lib/utils/find-chrome.ts
|
|
13
|
+
import { accessSync, constants } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
function findChromeSync() {
|
|
16
|
+
if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
|
|
17
|
+
for (const dir of PATH_DIRS) {
|
|
18
|
+
for (const name of CANDIDATES) {
|
|
19
|
+
const fullPath = join(dir, name);
|
|
20
|
+
try {
|
|
21
|
+
accessSync(fullPath, constants.X_OK);
|
|
22
|
+
return fullPath;
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function findChrome() {
|
|
30
|
+
return Promise.resolve(findChromeSync());
|
|
31
|
+
}
|
|
32
|
+
var CANDIDATES, PATH_DIRS;
|
|
33
|
+
var init_find_chrome = __esm({
|
|
34
|
+
"lib/utils/find-chrome.ts"() {
|
|
35
|
+
CANDIDATES = ["google-chrome-stable", "google-chrome", "chromium", "chromium-browser"];
|
|
36
|
+
PATH_DIRS = (process.env.PATH || "").split(":").filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// lib/utils/pre-launch-chrome.ts
|
|
41
|
+
import { spawn } from "node:child_process";
|
|
42
|
+
function preLaunchChrome(chromePath, args) {
|
|
43
|
+
if (!chromePath) return Promise.resolve(null);
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const proc = spawn(chromePath, ["--remote-debugging-port=0", "--headless=new", ...args], {
|
|
46
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
47
|
+
});
|
|
48
|
+
let buffer = "";
|
|
49
|
+
proc.stderr.on("data", (chunk) => {
|
|
50
|
+
buffer += chunk.toString();
|
|
51
|
+
const match = buffer.match(CDP_URL_REGEX);
|
|
52
|
+
if (match) {
|
|
53
|
+
proc.unref();
|
|
54
|
+
proc.stderr.unref();
|
|
55
|
+
resolve({ proc, cdpEndpoint: match[1] });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
proc.on("error", () => resolve(null));
|
|
59
|
+
proc.on("close", () => resolve(null));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
var CDP_URL_REGEX;
|
|
63
|
+
var init_pre_launch_chrome = __esm({
|
|
64
|
+
"lib/utils/pre-launch-chrome.ts"() {
|
|
65
|
+
CDP_URL_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// lib/utils/chromium-args.ts
|
|
70
|
+
var chromium_args_default;
|
|
71
|
+
var init_chromium_args = __esm({
|
|
72
|
+
"lib/utils/chromium-args.ts"() {
|
|
73
|
+
chromium_args_default = [
|
|
74
|
+
"--no-sandbox",
|
|
75
|
+
"--disable-gpu",
|
|
76
|
+
"--window-size=1440,900",
|
|
77
|
+
"--disable-extensions",
|
|
78
|
+
"--disable-sync",
|
|
79
|
+
"--no-first-run",
|
|
80
|
+
"--disable-default-apps",
|
|
81
|
+
"--mute-audio",
|
|
82
|
+
"--disable-background-networking",
|
|
83
|
+
"--disable-background-timer-throttling",
|
|
84
|
+
"--disable-renderer-backgrounding",
|
|
85
|
+
"--disable-dev-shm-usage",
|
|
86
|
+
"--disable-translate",
|
|
87
|
+
"--metrics-recording-only",
|
|
88
|
+
"--disable-hang-monitor"
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// lib/utils/perf-logger.ts
|
|
94
|
+
function perfLog(label, ...details) {
|
|
95
|
+
if (!isPerfTracing) return;
|
|
96
|
+
const elapsed = Date.now() - processStart;
|
|
97
|
+
const suffix = details.length ? " " + details.join(" ") : "";
|
|
98
|
+
process.stderr.write(`[perf +${elapsed}ms] ${label}${suffix}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
var isPerfTracing, processStart;
|
|
102
|
+
var init_perf_logger = __esm({
|
|
103
|
+
"lib/utils/perf-logger.ts"() {
|
|
104
|
+
isPerfTracing = process.argv.includes("--trace-perf");
|
|
105
|
+
processStart = isPerfTracing ? Date.now() : 0;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// lib/utils/early-chrome.ts
|
|
110
|
+
var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, earlyChromeProcRef, earlyBrowserPromise;
|
|
111
|
+
var init_early_chrome = __esm({
|
|
112
|
+
"lib/utils/early-chrome.ts"() {
|
|
113
|
+
init_find_chrome();
|
|
114
|
+
init_pre_launch_chrome();
|
|
115
|
+
init_chromium_args();
|
|
116
|
+
init_perf_logger();
|
|
117
|
+
NON_RUN_COMMANDS = /* @__PURE__ */ new Set(["help", "h", "p", "print", "new", "n", "g", "generate", "init"]);
|
|
118
|
+
isRunCommand = Boolean(process.argv[2]) && !NON_RUN_COMMANDS.has(process.argv[2]);
|
|
119
|
+
browserFromArgv = process.argv.find((arg) => arg.startsWith("--browser="))?.split("=")[1] || "chromium";
|
|
120
|
+
earlyChromeProcRef = null;
|
|
121
|
+
process.on("exit", () => earlyChromeProcRef?.kill());
|
|
122
|
+
perfLog("early-chrome.js: module evaluated");
|
|
123
|
+
earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
|
|
124
|
+
perfLog("early-chrome.js: findChrome resolved", chromePath);
|
|
125
|
+
return preLaunchChrome(chromePath, chromium_args_default);
|
|
126
|
+
}).then((info) => {
|
|
127
|
+
perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
|
|
128
|
+
if (info) earlyChromeProcRef = info.proc;
|
|
129
|
+
return info;
|
|
130
|
+
}) : Promise.resolve(null);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// lib/utils/color.ts
|
|
135
|
+
function createColors(enabled2) {
|
|
136
|
+
const c = (open, close) => (text) => enabled2 ? `\x1B[${open}m${text}\x1B[${close}m` : String(text);
|
|
137
|
+
const red2 = c(31, 39);
|
|
138
|
+
const green2 = c(32, 39);
|
|
139
|
+
const yellow2 = c(33, 39);
|
|
140
|
+
const blue2 = c(34, 39);
|
|
141
|
+
const magenta2 = ((text) => {
|
|
142
|
+
if (text !== void 0) return enabled2 ? `\x1B[35m${text}\x1B[39m` : String(text);
|
|
143
|
+
return { bold: (t) => enabled2 ? `\x1B[35m\x1B[1m${t}\x1B[22m\x1B[39m` : String(t) };
|
|
144
|
+
});
|
|
145
|
+
return { red: red2, green: green2, yellow: yellow2, blue: blue2, magenta: magenta2 };
|
|
146
|
+
}
|
|
147
|
+
function red(text) {
|
|
148
|
+
return _c.red(text);
|
|
149
|
+
}
|
|
150
|
+
function green(text) {
|
|
151
|
+
return _c.green(text);
|
|
152
|
+
}
|
|
153
|
+
function yellow(text) {
|
|
154
|
+
return _c.yellow(text);
|
|
155
|
+
}
|
|
156
|
+
function blue(text) {
|
|
157
|
+
return _c.blue(text);
|
|
158
|
+
}
|
|
159
|
+
function magenta(text) {
|
|
160
|
+
return _c.magenta(text);
|
|
161
|
+
}
|
|
162
|
+
var enabled, _c;
|
|
163
|
+
var init_color = __esm({
|
|
164
|
+
"lib/utils/color.ts"() {
|
|
165
|
+
enabled = !process.env.NODE_DISABLE_COLORS && process.env.NO_COLOR == null && process.env.TERM !== "dumb" && (process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "0" || !!process.stdout?.isTTY);
|
|
166
|
+
_c = createColors(enabled);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// lib/utils/path-exists.ts
|
|
171
|
+
import fs from "node:fs/promises";
|
|
172
|
+
async function pathExists(path5) {
|
|
173
|
+
try {
|
|
174
|
+
await fs.access(path5);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var init_path_exists = __esm({
|
|
181
|
+
"lib/utils/path-exists.ts"() {
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// lib/utils/read-boilerplate.ts
|
|
186
|
+
import fs2 from "node:fs/promises";
|
|
187
|
+
import { dirname, join as join2 } from "node:path";
|
|
188
|
+
import { fileURLToPath } from "node:url";
|
|
189
|
+
async function readBoilerplate(relativePath) {
|
|
190
|
+
const sea = await import("node:sea").catch(() => null);
|
|
191
|
+
if (sea?.isSea()) return sea.getAsset(relativePath, "utf8");
|
|
192
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
193
|
+
return (await fs2.readFile(join2(__dirname, "../../templates", relativePath))).toString();
|
|
194
|
+
}
|
|
195
|
+
var init_read_boilerplate = __esm({
|
|
196
|
+
"lib/utils/read-boilerplate.ts"() {
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// lib/utils/find-internal-assets-from-html.ts
|
|
201
|
+
function findInternalAssetsFromHTML(htmlContent) {
|
|
202
|
+
const links = [...htmlContent.matchAll(LINK_HREF_REGEX)].map((m) => m[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
203
|
+
const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)].map((m) => m[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
204
|
+
return links.concat(scripts);
|
|
205
|
+
}
|
|
206
|
+
var ABSOLUTE_URL_REGEX, SCRIPT_SRC_REGEX, LINK_HREF_REGEX;
|
|
207
|
+
var init_find_internal_assets_from_html = __esm({
|
|
208
|
+
"lib/utils/find-internal-assets-from-html.ts"() {
|
|
209
|
+
ABSOLUTE_URL_REGEX = /^(?:[a-z]+:)?\/\//i;
|
|
210
|
+
SCRIPT_SRC_REGEX = /<script[^>]+\bsrc=['"]([^'"]+)['"]/gi;
|
|
211
|
+
LINK_HREF_REGEX = /<link[^>]+\bhref=['"]([^'"]+)['"]/gi;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// lib/tap/dump-yaml.ts
|
|
216
|
+
function needsQuoting(str) {
|
|
217
|
+
return NEEDS_QUOTING.test(str);
|
|
218
|
+
}
|
|
219
|
+
function dumpString(str, indent) {
|
|
220
|
+
if (str === "") return "''";
|
|
221
|
+
if (str.includes("\n")) {
|
|
222
|
+
return "|-\n" + str.replace(/^/gm, `${indent} `);
|
|
223
|
+
}
|
|
224
|
+
if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
|
|
225
|
+
return str;
|
|
226
|
+
}
|
|
227
|
+
function dumpValue(value, indent) {
|
|
228
|
+
if (value === null || value === void 0) return "null";
|
|
229
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
230
|
+
if (typeof value === "string") return dumpString(value, indent);
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
if (value.length === 0) return "[]";
|
|
233
|
+
const next2 = `${indent} `;
|
|
234
|
+
return "\n" + value.map((v) => `${next2}- ${dumpValue(v, next2)}`).join("\n");
|
|
235
|
+
}
|
|
236
|
+
const entries = Object.entries(value);
|
|
237
|
+
if (entries.length === 0) return "{}";
|
|
238
|
+
const next = `${indent} `;
|
|
239
|
+
return "\n" + entries.map(([k, v]) => `${next}${k}: ${dumpValue(v, next)}`).join("\n");
|
|
240
|
+
}
|
|
241
|
+
function yamlLine(key, value) {
|
|
242
|
+
const v = dumpValue(value, "");
|
|
243
|
+
return v[0] === "\n" ? `${key}:${v}
|
|
244
|
+
` : `${key}: ${v}
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
function dumpYaml({
|
|
248
|
+
name,
|
|
249
|
+
actual,
|
|
250
|
+
expected,
|
|
251
|
+
message,
|
|
252
|
+
stack,
|
|
253
|
+
at
|
|
254
|
+
}) {
|
|
255
|
+
return `name: ${dumpString(name, "")}
|
|
256
|
+
` + yamlLine("actual", actual) + yamlLine("expected", expected) + yamlLine("message", message) + yamlLine("stack", stack) + yamlLine("at", at);
|
|
257
|
+
}
|
|
258
|
+
var NEEDS_QUOTING;
|
|
259
|
+
var init_dump_yaml = __esm({
|
|
260
|
+
"lib/tap/dump-yaml.ts"() {
|
|
261
|
+
NEEDS_QUOTING = /^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// lib/utils/indent-string.ts
|
|
266
|
+
function indentString(string, count = 1, options = {}) {
|
|
267
|
+
const { indent = " ", includeEmptyLines = false } = options;
|
|
268
|
+
if (count <= 0) {
|
|
269
|
+
return string;
|
|
270
|
+
}
|
|
271
|
+
const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
|
|
272
|
+
return string.replace(regex, indent.repeat(count));
|
|
273
|
+
}
|
|
274
|
+
var init_indent_string = __esm({
|
|
275
|
+
"lib/utils/indent-string.ts"() {
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// lib/tap/display-test-result.ts
|
|
280
|
+
function TAPDisplayTestResult(COUNTER, details) {
|
|
281
|
+
COUNTER.testCount++;
|
|
282
|
+
if (details.status === "skipped") {
|
|
283
|
+
COUNTER.skipCount++;
|
|
284
|
+
console.log(`ok ${COUNTER.testCount}`, details.fullName.join(" | "), "# skip");
|
|
285
|
+
} else if (details.status === "todo") {
|
|
286
|
+
console.log(`not ok ${COUNTER.testCount}`, details.fullName.join(" | "), "# skip");
|
|
287
|
+
} else if (details.status === "failed") {
|
|
288
|
+
COUNTER.failCount++;
|
|
289
|
+
console.log(
|
|
290
|
+
`not ok ${COUNTER.testCount}`,
|
|
291
|
+
details.fullName.join(" | "),
|
|
292
|
+
`# (${details.runtime.toFixed(0)} ms)`
|
|
293
|
+
);
|
|
294
|
+
details.assertions.forEach((assertion, index) => {
|
|
295
|
+
if (!assertion.passed && assertion.todo === false) {
|
|
296
|
+
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
297
|
+
const stack = assertion.stack?.match(/\(.+\)/g);
|
|
298
|
+
console.log(" ---");
|
|
299
|
+
console.log(
|
|
300
|
+
indentString(
|
|
301
|
+
dumpYaml({
|
|
302
|
+
name: `Assertion #${index + 1}`,
|
|
303
|
+
actual: assertion.actual ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
304
|
+
expected: assertion.expected ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
305
|
+
message: assertion.message || null,
|
|
306
|
+
stack: assertion.stack || null,
|
|
307
|
+
at: stack ? stack[0].replace("(file://", "").replace(")", "") : null
|
|
308
|
+
}),
|
|
309
|
+
4
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
console.log(" ...");
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
} else if (details.status === "passed") {
|
|
316
|
+
COUNTER.passCount++;
|
|
317
|
+
console.log(
|
|
318
|
+
`ok ${COUNTER.testCount}`,
|
|
319
|
+
details.fullName.join(" | "),
|
|
320
|
+
`# (${details.runtime.toFixed(0)} ms)`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function getCircularReplacer() {
|
|
325
|
+
const ancestors = [];
|
|
326
|
+
return function(_key, value) {
|
|
327
|
+
if (typeof value !== "object" || value === null) {
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
331
|
+
ancestors.pop();
|
|
332
|
+
}
|
|
333
|
+
if (ancestors.includes(value)) {
|
|
334
|
+
return "[Circular]";
|
|
335
|
+
}
|
|
336
|
+
ancestors.push(value);
|
|
337
|
+
return value;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
var init_display_test_result = __esm({
|
|
341
|
+
"lib/tap/display-test-result.ts"() {
|
|
342
|
+
init_dump_yaml();
|
|
343
|
+
init_indent_string();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// lib/setup/bind-server-to-port.ts
|
|
348
|
+
async function bindServerToPort(server, config) {
|
|
349
|
+
await server.listen(0);
|
|
350
|
+
config.port = server._server.address().port;
|
|
351
|
+
return server;
|
|
352
|
+
}
|
|
353
|
+
var init_bind_server_to_port = __esm({
|
|
354
|
+
"lib/setup/bind-server-to-port.ts"() {
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// lib/servers/http.ts
|
|
359
|
+
import http from "node:http";
|
|
360
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
361
|
+
var MIME_TYPES, HTTPServer;
|
|
362
|
+
var init_http = __esm({
|
|
363
|
+
"lib/servers/http.ts"() {
|
|
364
|
+
init_bind_server_to_port();
|
|
365
|
+
MIME_TYPES = {
|
|
366
|
+
html: "text/html; charset=UTF-8",
|
|
367
|
+
js: "application/javascript",
|
|
368
|
+
css: "text/css",
|
|
369
|
+
png: "image/png",
|
|
370
|
+
jpg: "image/jpg",
|
|
371
|
+
gif: "image/gif",
|
|
372
|
+
ico: "image/x-icon",
|
|
373
|
+
svg: "image/svg+xml"
|
|
374
|
+
};
|
|
375
|
+
HTTPServer = class {
|
|
376
|
+
/** Registered routes keyed by HTTP method then path. */
|
|
377
|
+
routes;
|
|
378
|
+
/** Registered middleware functions, applied in order before each route handler. */
|
|
379
|
+
middleware;
|
|
380
|
+
/** Underlying Node.js HTTP server instance. */
|
|
381
|
+
_server;
|
|
382
|
+
/** WebSocket server attached to the HTTP server for live-reload broadcasts. */
|
|
383
|
+
wss;
|
|
384
|
+
/**
|
|
385
|
+
* Creates and starts a plain `http.createServer` instance on the given port.
|
|
386
|
+
* @returns {Promise<object>}
|
|
387
|
+
*/
|
|
388
|
+
static serve(config = {
|
|
389
|
+
port: 1234
|
|
390
|
+
}, handler) {
|
|
391
|
+
const onListen = config.onListen || ((_server) => {
|
|
392
|
+
});
|
|
393
|
+
const onError = config.onError || ((_error) => {
|
|
394
|
+
});
|
|
395
|
+
return new Promise((resolve, reject) => {
|
|
396
|
+
const server = http.createServer((req, res) => {
|
|
397
|
+
return handler(req, res);
|
|
398
|
+
});
|
|
399
|
+
server.on("error", (error) => {
|
|
400
|
+
onError(error);
|
|
401
|
+
reject(error);
|
|
402
|
+
}).once("listening", () => {
|
|
403
|
+
onListen(Object.assign({ hostname: "127.0.0.1", server }, config));
|
|
404
|
+
resolve(server);
|
|
405
|
+
});
|
|
406
|
+
server.wss = new WebSocketServer({ server });
|
|
407
|
+
server.wss.on("error", (error) => {
|
|
408
|
+
console.log("# [WebSocketServer] Error:");
|
|
409
|
+
console.trace(error);
|
|
410
|
+
});
|
|
411
|
+
bindServerToPort(server, config);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
constructor() {
|
|
415
|
+
this.routes = {
|
|
416
|
+
GET: {},
|
|
417
|
+
POST: {},
|
|
418
|
+
DELETE: {},
|
|
419
|
+
PUT: {}
|
|
420
|
+
};
|
|
421
|
+
this.middleware = [];
|
|
422
|
+
this._server = http.createServer((req, res) => {
|
|
423
|
+
req.send = (data) => {
|
|
424
|
+
res.setHeader("Content-Type", "text/plain");
|
|
425
|
+
res.end(data);
|
|
426
|
+
};
|
|
427
|
+
res.json = (data) => {
|
|
428
|
+
res.setHeader("Content-Type", "application/json");
|
|
429
|
+
res.end(JSON.stringify(data));
|
|
430
|
+
};
|
|
431
|
+
return this.#handleRequest(req, res);
|
|
432
|
+
});
|
|
433
|
+
this.wss = new WebSocketServer({ server: this._server });
|
|
434
|
+
this.wss.on("error", (error) => {
|
|
435
|
+
console.log("# [WebSocketServer] Error:");
|
|
436
|
+
console.log(error);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Closes the underlying HTTP server and all active connections, returning a
|
|
441
|
+
* Promise that resolves once the server is fully closed.
|
|
442
|
+
* @returns {Promise<void>}
|
|
443
|
+
*/
|
|
444
|
+
close() {
|
|
445
|
+
this._server.closeAllConnections?.();
|
|
446
|
+
return new Promise((resolve) => this._server.close(resolve));
|
|
447
|
+
}
|
|
448
|
+
/** Registers a GET route handler. */
|
|
449
|
+
get(path5, handler) {
|
|
450
|
+
this.#registerRouteHandler("GET", path5, handler);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Starts listening on the given port (0 = OS-assigned).
|
|
454
|
+
* @returns {Promise<void>}
|
|
455
|
+
*/
|
|
456
|
+
listen(port = 0, callback = () => {
|
|
457
|
+
}) {
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
const onError = (err) => {
|
|
460
|
+
this._server.off("listening", onListening);
|
|
461
|
+
reject(err);
|
|
462
|
+
};
|
|
463
|
+
const onListening = () => {
|
|
464
|
+
this._server.off("error", onError);
|
|
465
|
+
resolve(callback());
|
|
466
|
+
};
|
|
467
|
+
this._server.once("error", onError);
|
|
468
|
+
this._server.once("listening", onListening);
|
|
469
|
+
this._server.listen(port);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/** Broadcasts a message to all connected WebSocket clients. */
|
|
473
|
+
publish(data) {
|
|
474
|
+
this.wss.clients.forEach((client) => {
|
|
475
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
476
|
+
client.send(data);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** Registers a POST route handler. */
|
|
481
|
+
post(path5, handler) {
|
|
482
|
+
this.#registerRouteHandler("POST", path5, handler);
|
|
483
|
+
}
|
|
484
|
+
/** Registers a DELETE route handler. */
|
|
485
|
+
delete(path5, handler) {
|
|
486
|
+
this.#registerRouteHandler("DELETE", path5, handler);
|
|
487
|
+
}
|
|
488
|
+
/** Registers a PUT route handler. */
|
|
489
|
+
put(path5, handler) {
|
|
490
|
+
this.#registerRouteHandler("PUT", path5, handler);
|
|
491
|
+
}
|
|
492
|
+
/** Adds a middleware function to the chain. */
|
|
493
|
+
use(middleware) {
|
|
494
|
+
this.middleware.push(middleware);
|
|
495
|
+
}
|
|
496
|
+
#registerRouteHandler(method, path5, handler) {
|
|
497
|
+
if (!this.routes[method]) {
|
|
498
|
+
this.routes[method] = {};
|
|
499
|
+
}
|
|
500
|
+
this.routes[method][path5] = {
|
|
501
|
+
path: path5,
|
|
502
|
+
handler,
|
|
503
|
+
paramNames: this.#extractParamNames(path5),
|
|
504
|
+
isWildcard: path5 === "/*"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
#handleRequest(req, res) {
|
|
508
|
+
const { method, url } = req;
|
|
509
|
+
const urlObj = new URL(url, "http://localhost");
|
|
510
|
+
const pathname = urlObj.pathname;
|
|
511
|
+
req.path = pathname;
|
|
512
|
+
req.query = Object.fromEntries(urlObj.searchParams);
|
|
513
|
+
const matchingRoute = this.#findRouteHandler(method, pathname);
|
|
514
|
+
if (matchingRoute) {
|
|
515
|
+
req.params = this.#extractParams(matchingRoute, pathname);
|
|
516
|
+
this.#runMiddleware(req, res, matchingRoute.handler);
|
|
517
|
+
} else {
|
|
518
|
+
res.statusCode = 404;
|
|
519
|
+
res.setHeader("Content-Type", "text/plain");
|
|
520
|
+
res.end("Not found");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
#runMiddleware(req, res, callback) {
|
|
524
|
+
let index = 0;
|
|
525
|
+
const next = () => {
|
|
526
|
+
if (index >= this.middleware.length) {
|
|
527
|
+
callback(req, res);
|
|
528
|
+
} else {
|
|
529
|
+
const middleware = this.middleware[index];
|
|
530
|
+
index++;
|
|
531
|
+
middleware(req, res, next);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
next();
|
|
535
|
+
}
|
|
536
|
+
#findRouteHandler(method, url) {
|
|
537
|
+
const routes = this.routes[method];
|
|
538
|
+
if (!routes) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
return routes[url] || Object.values(routes).find((route) => {
|
|
542
|
+
const { path: path5, isWildcard } = route;
|
|
543
|
+
if (!isWildcard && !path5.includes(":")) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (isWildcard || this.#matchPathSegments(path5, url)) {
|
|
547
|
+
if (route.paramNames.length > 0) {
|
|
548
|
+
const regexPattern = this.#buildRegexPattern(path5, route.paramNames);
|
|
549
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
550
|
+
const regexMatches = regex.exec(url);
|
|
551
|
+
if (regexMatches) {
|
|
552
|
+
route.paramValues = regexMatches.slice(1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}) || routes["/*"] || null;
|
|
559
|
+
}
|
|
560
|
+
#matchPathSegments(path5, url) {
|
|
561
|
+
const pathSegments = path5.split("/");
|
|
562
|
+
const urlSegments = url.split("/");
|
|
563
|
+
if (pathSegments.length !== urlSegments.length) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
567
|
+
const pathSegment = pathSegments[i];
|
|
568
|
+
const urlSegment = urlSegments[i];
|
|
569
|
+
if (pathSegment.startsWith(":")) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (pathSegment !== urlSegment) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
#buildRegexPattern(path5, _paramNames) {
|
|
579
|
+
let regexPattern = path5.replace(/:[^/]+/g, "([^/]+)");
|
|
580
|
+
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
581
|
+
return regexPattern;
|
|
582
|
+
}
|
|
583
|
+
#extractParamNames(path5) {
|
|
584
|
+
const paramRegex = /:(\w+)/g;
|
|
585
|
+
const paramMatches = path5.match(paramRegex);
|
|
586
|
+
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
587
|
+
}
|
|
588
|
+
#extractParams(route, _url) {
|
|
589
|
+
const { paramNames, paramValues } = route;
|
|
590
|
+
const params = {};
|
|
591
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
592
|
+
params[paramNames[i]] = paramValues[i];
|
|
593
|
+
}
|
|
594
|
+
return params;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// lib/setup/web-server.ts
|
|
601
|
+
import fs7 from "node:fs";
|
|
602
|
+
import path3 from "node:path";
|
|
603
|
+
function setupWebServer(config, cachedContent) {
|
|
604
|
+
const STATIC_FILES_PATH = path3.join(config.projectRoot, config.output);
|
|
605
|
+
const server = new HTTPServer();
|
|
606
|
+
server.wss.on("connection", function connection(socket) {
|
|
607
|
+
socket.on("message", function message(data) {
|
|
608
|
+
const { event, details, abort } = JSON.parse(data);
|
|
609
|
+
if (event === "connection") {
|
|
610
|
+
if (!config._groupMode) console.log("TAP version 13");
|
|
611
|
+
config._resetTestTimeout?.();
|
|
612
|
+
} else if (event === "testEnd" && !abort) {
|
|
613
|
+
if (details.status === "failed") {
|
|
614
|
+
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
615
|
+
}
|
|
616
|
+
config._resetTestTimeout?.();
|
|
617
|
+
TAPDisplayTestResult(config.COUNTER, details);
|
|
618
|
+
} else if (event === "done") {
|
|
619
|
+
if (typeof config._testRunDone === "function") {
|
|
620
|
+
config._testRunDone();
|
|
621
|
+
config._testRunDone = null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
server.get("/", async (_req, res) => {
|
|
627
|
+
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
628
|
+
const htmlContent = escapeAndInjectTestsToHTML(
|
|
629
|
+
replaceAssetPaths(
|
|
630
|
+
cachedContent.mainHTML.html,
|
|
631
|
+
cachedContent.mainHTML.filePath,
|
|
632
|
+
config.projectRoot
|
|
633
|
+
),
|
|
634
|
+
TEST_RUNTIME_TO_INJECT,
|
|
635
|
+
cachedContent.allTestCode
|
|
636
|
+
);
|
|
637
|
+
res.write(htmlContent);
|
|
638
|
+
res.end();
|
|
639
|
+
return await fsPromise.writeFile(
|
|
640
|
+
`${config.projectRoot}/${config.output}/index.html`,
|
|
641
|
+
htmlContent
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
server.get("/qunitx.html", async (_req, res) => {
|
|
645
|
+
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
646
|
+
const htmlContent = escapeAndInjectTestsToHTML(
|
|
647
|
+
replaceAssetPaths(
|
|
648
|
+
cachedContent.mainHTML.html,
|
|
649
|
+
cachedContent.mainHTML.filePath,
|
|
650
|
+
config.projectRoot
|
|
651
|
+
),
|
|
652
|
+
TEST_RUNTIME_TO_INJECT,
|
|
653
|
+
cachedContent.filteredTestCode
|
|
654
|
+
);
|
|
655
|
+
res.write(htmlContent);
|
|
656
|
+
res.end();
|
|
657
|
+
return await fsPromise.writeFile(
|
|
658
|
+
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
659
|
+
htmlContent
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
server.get("/*", async (req, res) => {
|
|
663
|
+
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
664
|
+
if (possibleDynamicHTML) {
|
|
665
|
+
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
666
|
+
const htmlContent = escapeAndInjectTestsToHTML(
|
|
667
|
+
possibleDynamicHTML,
|
|
668
|
+
TEST_RUNTIME_TO_INJECT,
|
|
669
|
+
cachedContent.allTestCode
|
|
670
|
+
);
|
|
671
|
+
res.write(htmlContent);
|
|
672
|
+
res.end();
|
|
673
|
+
return await fsPromise.writeFile(
|
|
674
|
+
`${config.projectRoot}/${config.output}${req.path}`,
|
|
675
|
+
htmlContent
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const url = req.url;
|
|
679
|
+
const requestStartedAt = /* @__PURE__ */ new Date();
|
|
680
|
+
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
681
|
+
const statusCode = await pathExists(filePath) ? 200 : 404;
|
|
682
|
+
res.writeHead(statusCode, {
|
|
683
|
+
"Content-Type": req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path3.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html
|
|
684
|
+
});
|
|
685
|
+
if (statusCode === 404) {
|
|
686
|
+
res.end();
|
|
687
|
+
} else {
|
|
688
|
+
fs7.createReadStream(filePath).pipe(res);
|
|
689
|
+
}
|
|
690
|
+
console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms`);
|
|
691
|
+
});
|
|
692
|
+
return server;
|
|
693
|
+
}
|
|
694
|
+
function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
695
|
+
const assetPaths = findInternalAssetsFromHTML(html);
|
|
696
|
+
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
697
|
+
return assetPaths.reduce((result, assetPath) => {
|
|
698
|
+
const normalizedFullAbsolutePath = path3.normalize(`${htmlDirectory}/${assetPath}`);
|
|
699
|
+
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
700
|
+
}, html);
|
|
701
|
+
}
|
|
702
|
+
function testRuntimeToInject(port, config) {
|
|
703
|
+
return `<script>
|
|
704
|
+
window.testTimeout = 0;
|
|
705
|
+
setInterval(() => {
|
|
706
|
+
window.testTimeout = window.testTimeout + 1000;
|
|
707
|
+
}, 1000);
|
|
708
|
+
|
|
709
|
+
(function() {
|
|
710
|
+
let wsRetryCount = 0;
|
|
711
|
+
const WS_MAX_RETRIES = 50; // 500ms total before giving up
|
|
712
|
+
|
|
713
|
+
function setupWebSocket() {
|
|
714
|
+
try {
|
|
715
|
+
window.socket = new WebSocket('ws://localhost:${port}');
|
|
716
|
+
} catch (error) {
|
|
717
|
+
console.log(error);
|
|
718
|
+
retryOrFail();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
window.socket.addEventListener('open', function() {
|
|
723
|
+
setupQUnit();
|
|
724
|
+
});
|
|
725
|
+
window.socket.addEventListener('error', function() {
|
|
726
|
+
retryOrFail();
|
|
727
|
+
});
|
|
728
|
+
window.socket.addEventListener('message', function(messageEvent) {
|
|
729
|
+
if (!window.IS_PLAYWRIGHT && messageEvent.data === 'refresh') {
|
|
730
|
+
window.location.reload(true);
|
|
731
|
+
} else if (window.IS_PLAYWRIGHT && messageEvent.data === 'abort') {
|
|
732
|
+
window.abortQUnit = true;
|
|
733
|
+
window.QUnit.config.queue.length = 0;
|
|
734
|
+
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function retryOrFail() {
|
|
740
|
+
wsRetryCount++;
|
|
741
|
+
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
742
|
+
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
743
|
+
window.testTimeout = ${config.timeout};
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
window.setTimeout(setupWebSocket, 10);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
setupWebSocket();
|
|
750
|
+
})();
|
|
751
|
+
|
|
752
|
+
{{allTestCode}}
|
|
753
|
+
|
|
754
|
+
function getCircularReplacer() {
|
|
755
|
+
const ancestors = [];
|
|
756
|
+
return function (key, value) {
|
|
757
|
+
if (typeof value !== "object" || value === null) {
|
|
758
|
+
return value;
|
|
759
|
+
}
|
|
760
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
761
|
+
ancestors.pop();
|
|
762
|
+
}
|
|
763
|
+
if (ancestors.includes(value)) {
|
|
764
|
+
return "[Circular]";
|
|
765
|
+
}
|
|
766
|
+
ancestors.push(value);
|
|
767
|
+
return value;
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function setupQUnit() {
|
|
772
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
773
|
+
|
|
774
|
+
if (!window.QUnit) {
|
|
775
|
+
console.log('QUnit not found after WebSocket connected');
|
|
776
|
+
window.testTimeout = ${config.timeout};
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
781
|
+
if (window.IS_PLAYWRIGHT) {
|
|
782
|
+
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
|
|
786
|
+
if (window.IS_PLAYWRIGHT) {
|
|
787
|
+
window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
window.QUnit.on('testStart', (details) => {
|
|
791
|
+
window.QUNIT_RESULT.totalTests++;
|
|
792
|
+
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
793
|
+
});
|
|
794
|
+
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
795
|
+
window.testTimeout = 0;
|
|
796
|
+
window.QUNIT_RESULT.finishedTests++;
|
|
797
|
+
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
798
|
+
window.QUNIT_RESULT.currentTest = null;
|
|
799
|
+
if (window.IS_PLAYWRIGHT) {
|
|
800
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
801
|
+
|
|
802
|
+
if (${config.failFast} && details.status === 'failed') {
|
|
803
|
+
window.QUnit.config.queue.length = 0;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
window.QUnit.done((details) => {
|
|
808
|
+
if (window.IS_PLAYWRIGHT) {
|
|
809
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
810
|
+
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
811
|
+
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
812
|
+
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
813
|
+
// Setting testTimeout after done caused a race: under CI load, waitForFunction could
|
|
814
|
+
// win before Node.js processed the WS done message, dropping all testEnd events.
|
|
815
|
+
} else {
|
|
816
|
+
window.testTimeout = ${config.timeout};
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
window.QUnit.start();
|
|
821
|
+
}
|
|
822
|
+
</script>`;
|
|
823
|
+
}
|
|
824
|
+
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testContentCode) {
|
|
825
|
+
return html.replace(
|
|
826
|
+
"{{content}}",
|
|
827
|
+
testRuntimeCode.replace("{{allTestCode}}", testContentCode).replace("</script>", "</script>")
|
|
828
|
+
// NOTE: remove this when simple-html-tokenizer PR gets merged
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
var fsPromise;
|
|
832
|
+
var init_web_server = __esm({
|
|
833
|
+
"lib/setup/web-server.ts"() {
|
|
834
|
+
init_find_internal_assets_from_html();
|
|
835
|
+
init_display_test_result();
|
|
836
|
+
init_path_exists();
|
|
837
|
+
init_http();
|
|
838
|
+
fsPromise = fs7.promises;
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// lib/setup/browser.ts
|
|
843
|
+
async function launchBrowser(config) {
|
|
844
|
+
const browserName = config.browser || "chromium";
|
|
845
|
+
if (browserName === "chromium") {
|
|
846
|
+
const waitStart = Date.now();
|
|
847
|
+
const [playwrightCore2, earlyChrome] = await Promise.all([
|
|
848
|
+
playwrightCorePromise,
|
|
849
|
+
earlyBrowserPromise
|
|
850
|
+
]);
|
|
851
|
+
perfLog(
|
|
852
|
+
`browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
|
|
853
|
+
earlyChrome?.cdpEndpoint ?? null
|
|
854
|
+
);
|
|
855
|
+
if (earlyChrome) {
|
|
856
|
+
const connectStart = Date.now();
|
|
857
|
+
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
858
|
+
endpointURL: earlyChrome.cdpEndpoint
|
|
859
|
+
});
|
|
860
|
+
perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
|
|
861
|
+
return browser;
|
|
862
|
+
}
|
|
863
|
+
const executablePath = await findChrome();
|
|
864
|
+
const launchOptions = { args: chromium_args_default, headless: true };
|
|
865
|
+
if (executablePath) launchOptions.executablePath = executablePath;
|
|
866
|
+
return playwrightCore2.chromium.launch(launchOptions);
|
|
867
|
+
}
|
|
868
|
+
const playwrightCore = await playwrightCorePromise;
|
|
869
|
+
return playwrightCore[browserName].launch({ headless: true });
|
|
870
|
+
}
|
|
871
|
+
async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
872
|
+
const setupStart = Date.now();
|
|
873
|
+
const [server, resolvedExistingBrowser] = await Promise.all([
|
|
874
|
+
setupWebServer(config, cachedContent),
|
|
875
|
+
Promise.resolve(existingBrowser)
|
|
876
|
+
]);
|
|
877
|
+
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
878
|
+
const browser = resolvedExistingBrowser || await launchBrowser(config);
|
|
879
|
+
const pageStart = Date.now();
|
|
880
|
+
const [page] = await Promise.all([browser.newPage(), bindServerToPort(server, config)]);
|
|
881
|
+
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
882
|
+
await page.addInitScript(() => {
|
|
883
|
+
window.IS_PLAYWRIGHT = true;
|
|
884
|
+
});
|
|
885
|
+
page.on("console", async (msg) => {
|
|
886
|
+
if (!config.debug) return;
|
|
887
|
+
try {
|
|
888
|
+
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
|
|
889
|
+
console.log(...values);
|
|
890
|
+
} catch {
|
|
891
|
+
console.log(msg.text());
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
page.on("pageerror", (error) => {
|
|
895
|
+
console.log(error.toString());
|
|
896
|
+
console.error(error.toString());
|
|
897
|
+
});
|
|
898
|
+
return { server, browser, page };
|
|
899
|
+
}
|
|
900
|
+
var playwrightCorePromise;
|
|
901
|
+
var init_browser = __esm({
|
|
902
|
+
"lib/setup/browser.ts"() {
|
|
903
|
+
init_web_server();
|
|
904
|
+
init_bind_server_to_port();
|
|
905
|
+
init_find_chrome();
|
|
906
|
+
init_chromium_args();
|
|
907
|
+
init_early_chrome();
|
|
908
|
+
init_perf_logger();
|
|
909
|
+
playwrightCorePromise = import("playwright-core");
|
|
910
|
+
perfLog("browser.js: playwright-core import started");
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// lib/utils/time-counter.ts
|
|
915
|
+
function timeCounter() {
|
|
916
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
917
|
+
return {
|
|
918
|
+
startTime,
|
|
919
|
+
stop: () => +/* @__PURE__ */ new Date() - +startTime
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
var init_time_counter = __esm({
|
|
923
|
+
"lib/utils/time-counter.ts"() {
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// lib/utils/run-user-module.ts
|
|
928
|
+
async function runUserModule(modulePath, params, scriptPosition) {
|
|
929
|
+
try {
|
|
930
|
+
const func = await import(modulePath);
|
|
931
|
+
if (func) {
|
|
932
|
+
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
933
|
+
}
|
|
934
|
+
} catch (error) {
|
|
935
|
+
console.log("#", red(`QUnitX ${scriptPosition} script failed:`));
|
|
936
|
+
console.trace(error);
|
|
937
|
+
console.error(error);
|
|
938
|
+
return process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
var init_run_user_module = __esm({
|
|
942
|
+
"lib/utils/run-user-module.ts"() {
|
|
943
|
+
init_color();
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// lib/tap/display-final-result.ts
|
|
948
|
+
function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, timeTaken) {
|
|
949
|
+
console.log("");
|
|
950
|
+
console.log(`1..${testCount}`);
|
|
951
|
+
console.log(`# tests ${testCount}`);
|
|
952
|
+
console.log(`# pass ${passCount}`);
|
|
953
|
+
console.log(`# skip ${skipCount}`);
|
|
954
|
+
console.log(`# fail ${failCount}`);
|
|
955
|
+
console.log(`# duration ${timeTaken}`);
|
|
956
|
+
console.log("");
|
|
957
|
+
}
|
|
958
|
+
var init_display_final_result = __esm({
|
|
959
|
+
"lib/tap/display-final-result.ts"() {
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// lib/commands/run/tests-in-browser.ts
|
|
964
|
+
import fs8 from "node:fs/promises";
|
|
965
|
+
import esbuild from "esbuild";
|
|
966
|
+
async function buildTestBundle(config, cachedContent) {
|
|
967
|
+
const { projectRoot, output } = config;
|
|
968
|
+
const allTestFilePaths = Object.keys(config.fsTree);
|
|
969
|
+
await Promise.all([
|
|
970
|
+
esbuild.build({
|
|
971
|
+
stdin: {
|
|
972
|
+
contents: allTestFilePaths.map((f) => `import "${f}";`).join(""),
|
|
973
|
+
resolveDir: process.cwd()
|
|
974
|
+
},
|
|
975
|
+
bundle: true,
|
|
976
|
+
logLevel: "error",
|
|
977
|
+
outfile: `${projectRoot}/${output}/tests.js`,
|
|
978
|
+
keepNames: true,
|
|
979
|
+
sourcemap: config.debug || config.watch ? "inline" : false
|
|
980
|
+
}),
|
|
981
|
+
Promise.all(
|
|
982
|
+
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
983
|
+
const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
|
|
984
|
+
if (htmlPath !== "/") {
|
|
985
|
+
await fs8.rm(targetPath, { force: true, recursive: true });
|
|
986
|
+
await fs8.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
)
|
|
990
|
+
]);
|
|
991
|
+
cachedContent.allTestCode = await fs8.readFile(`${projectRoot}/${output}/tests.js`);
|
|
992
|
+
}
|
|
993
|
+
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
994
|
+
const { projectRoot, output } = config;
|
|
995
|
+
const allTestFilePaths = Object.keys(config.fsTree);
|
|
996
|
+
const runHasFilter = !!targetTestFilesToFilter;
|
|
997
|
+
if (!config._groupMode) {
|
|
998
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
|
|
999
|
+
}
|
|
1000
|
+
config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
|
|
1001
|
+
try {
|
|
1002
|
+
if (!cachedContent.allTestCode) {
|
|
1003
|
+
await buildTestBundle(config, cachedContent);
|
|
1004
|
+
}
|
|
1005
|
+
if (runHasFilter) {
|
|
1006
|
+
const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
|
|
1007
|
+
await buildFilteredTests(targetTestFilesToFilter, outputPath, config);
|
|
1008
|
+
cachedContent.filteredTestCode = (await fs8.readFile(outputPath)).toString();
|
|
1009
|
+
}
|
|
1010
|
+
const TIME_COUNTER = timeCounter();
|
|
1011
|
+
if (runHasFilter) {
|
|
1012
|
+
await runTestInsideHTMLFile("/qunitx.html", connections, config);
|
|
1013
|
+
} else {
|
|
1014
|
+
await Promise.all(
|
|
1015
|
+
cachedContent.htmlPathsToRunTests.map(
|
|
1016
|
+
(htmlPath) => runTestInsideHTMLFile(htmlPath, connections, config)
|
|
1017
|
+
)
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1021
|
+
if (!config._groupMode) {
|
|
1022
|
+
TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
|
|
1023
|
+
if (config.after) {
|
|
1024
|
+
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
1025
|
+
}
|
|
1026
|
+
if (!config.watch) {
|
|
1027
|
+
await Promise.all([
|
|
1028
|
+
connections.server && connections.server.close(),
|
|
1029
|
+
connections.browser && connections.browser.close()
|
|
1030
|
+
]);
|
|
1031
|
+
return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1036
|
+
console.log(error);
|
|
1037
|
+
const exception = new BundleError(error);
|
|
1038
|
+
if (config.watch) {
|
|
1039
|
+
console.log(`# ${exception}`);
|
|
1040
|
+
} else {
|
|
1041
|
+
throw exception;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return connections;
|
|
1045
|
+
}
|
|
1046
|
+
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1047
|
+
return esbuild.build({
|
|
1048
|
+
stdin: {
|
|
1049
|
+
contents: filteredTests.map((f) => `import "${f}";`).join(""),
|
|
1050
|
+
resolveDir: process.cwd()
|
|
1051
|
+
},
|
|
1052
|
+
bundle: true,
|
|
1053
|
+
logLevel: "error",
|
|
1054
|
+
outfile: outputPath,
|
|
1055
|
+
sourcemap: config.debug || config.watch ? "inline" : false
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
1059
|
+
let QUNIT_RESULT;
|
|
1060
|
+
let targetError;
|
|
1061
|
+
let timeoutHandle;
|
|
1062
|
+
try {
|
|
1063
|
+
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
1064
|
+
const testRaceResult = new Promise((resolve) => {
|
|
1065
|
+
config._testRunDone = () => resolve(false);
|
|
1066
|
+
config._resetTestTimeout = () => {
|
|
1067
|
+
clearTimeout(timeoutHandle);
|
|
1068
|
+
timeoutHandle = setTimeout(() => resolve(true), config.timeout);
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1071
|
+
await page.goto(`http://localhost:${config.port}${filePath}`, {
|
|
1072
|
+
timeout: config.timeout + 1e4
|
|
1073
|
+
});
|
|
1074
|
+
config._resetTestTimeout();
|
|
1075
|
+
await testRaceResult;
|
|
1076
|
+
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
targetError = error;
|
|
1079
|
+
console.log(error);
|
|
1080
|
+
console.error(error);
|
|
1081
|
+
} finally {
|
|
1082
|
+
clearTimeout(timeoutHandle);
|
|
1083
|
+
config._resetTestTimeout = null;
|
|
1084
|
+
}
|
|
1085
|
+
if (!QUNIT_RESULT || QUNIT_RESULT.totalTests === 0) {
|
|
1086
|
+
console.log(targetError);
|
|
1087
|
+
console.log("BROWSER: runtime error thrown during executing tests");
|
|
1088
|
+
console.error("BROWSER: runtime error thrown during executing tests");
|
|
1089
|
+
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
1090
|
+
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
1091
|
+
console.log(targetError);
|
|
1092
|
+
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1093
|
+
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1094
|
+
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
1095
|
+
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
1096
|
+
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
|
|
1100
|
+
if (!watchMode) {
|
|
1101
|
+
if (groupMode) {
|
|
1102
|
+
throw new Error("Browser test run failed");
|
|
1103
|
+
}
|
|
1104
|
+
await Promise.all([
|
|
1105
|
+
connections.server && connections.server.close(),
|
|
1106
|
+
connections.browser && connections.browser.close()
|
|
1107
|
+
]);
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
var BundleError;
|
|
1112
|
+
var init_tests_in_browser = __esm({
|
|
1113
|
+
"lib/commands/run/tests-in-browser.ts"() {
|
|
1114
|
+
init_color();
|
|
1115
|
+
init_time_counter();
|
|
1116
|
+
init_run_user_module();
|
|
1117
|
+
init_display_final_result();
|
|
1118
|
+
BundleError = class extends Error {
|
|
1119
|
+
constructor(message) {
|
|
1120
|
+
super(message);
|
|
1121
|
+
this.name = "BundleError";
|
|
1122
|
+
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
// lib/setup/file-watcher.ts
|
|
1129
|
+
import fs9 from "node:fs";
|
|
1130
|
+
import { stat } from "node:fs/promises";
|
|
1131
|
+
import path4 from "node:path";
|
|
1132
|
+
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1133
|
+
const extensions = config.extensions || ["js", "ts"];
|
|
1134
|
+
const fileWatchers = testFileLookupPaths.reduce((watchers, watchPath) => {
|
|
1135
|
+
let ready = false;
|
|
1136
|
+
const watcher = fs9.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1137
|
+
if (!ready || !filename) return;
|
|
1138
|
+
const fullPath = path4.join(watchPath, filename);
|
|
1139
|
+
if (eventType === "change") {
|
|
1140
|
+
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const s = await stat(fullPath);
|
|
1144
|
+
handleWatchEvent(
|
|
1145
|
+
config,
|
|
1146
|
+
extensions,
|
|
1147
|
+
s.isDirectory() ? "addDir" : "add",
|
|
1148
|
+
fullPath,
|
|
1149
|
+
onEventFunc,
|
|
1150
|
+
onFinishFunc
|
|
1151
|
+
);
|
|
1152
|
+
} catch {
|
|
1153
|
+
const event = config.fsTree && fullPath in config.fsTree ? "unlink" : "unlinkDir";
|
|
1154
|
+
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
setImmediate(() => {
|
|
1158
|
+
ready = true;
|
|
1159
|
+
});
|
|
1160
|
+
return Object.assign(watchers, { [watchPath]: watcher });
|
|
1161
|
+
}, {});
|
|
1162
|
+
return {
|
|
1163
|
+
fileWatchers,
|
|
1164
|
+
killFileWatchers() {
|
|
1165
|
+
Object.keys(fileWatchers).forEach((watcherKey) => fileWatchers[watcherKey].close());
|
|
1166
|
+
return fileWatchers;
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
1171
|
+
const isFileEvent = extensions.some((ext) => filePath.endsWith(`.${ext}`));
|
|
1172
|
+
if (!isFileEvent && event !== "unlinkDir") return;
|
|
1173
|
+
mutateFSTree(config.fsTree, event, filePath);
|
|
1174
|
+
console.log(
|
|
1175
|
+
"#",
|
|
1176
|
+
magenta().bold("==================================================================")
|
|
1177
|
+
);
|
|
1178
|
+
console.log("#", getEventColor(event), filePath.split(config.projectRoot)[1]);
|
|
1179
|
+
console.log(
|
|
1180
|
+
"#",
|
|
1181
|
+
magenta().bold("==================================================================")
|
|
1182
|
+
);
|
|
1183
|
+
if (!config._building) {
|
|
1184
|
+
config._building = true;
|
|
1185
|
+
const result = onEventFunc(event, filePath);
|
|
1186
|
+
if (!(result instanceof Promise)) {
|
|
1187
|
+
config._building = false;
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
result.then(() => {
|
|
1191
|
+
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
1192
|
+
}).catch((error) => {
|
|
1193
|
+
console.error("#", red("Build error:"), error.message || error);
|
|
1194
|
+
}).finally(() => config._building = false);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function mutateFSTree(fsTree, event, path5) {
|
|
1198
|
+
if (event === "add") {
|
|
1199
|
+
fsTree[path5] = null;
|
|
1200
|
+
} else if (event === "unlink") {
|
|
1201
|
+
delete fsTree[path5];
|
|
1202
|
+
} else if (event === "unlinkDir") {
|
|
1203
|
+
for (const treePath of Object.keys(fsTree)) {
|
|
1204
|
+
if (treePath.startsWith(path5)) delete fsTree[treePath];
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function getEventColor(event) {
|
|
1209
|
+
if (event === "change") {
|
|
1210
|
+
return yellow("CHANGED:");
|
|
1211
|
+
} else if (event === "add" || event === "addDir") {
|
|
1212
|
+
return green("ADDED:");
|
|
1213
|
+
} else if (event === "unlink" || event === "unlinkDir") {
|
|
1214
|
+
return red("REMOVED:");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
var init_file_watcher = __esm({
|
|
1218
|
+
"lib/setup/file-watcher.ts"() {
|
|
1219
|
+
init_color();
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// lib/utils/listen-to-keyboard-key.ts
|
|
1224
|
+
import process3 from "node:process";
|
|
1225
|
+
function listenToKeyboardKey(inputString, closure, options = { caseSensitive: false }) {
|
|
1226
|
+
if (!stdin.isTTY) return;
|
|
1227
|
+
stdin.setRawMode(true);
|
|
1228
|
+
stdin.resume();
|
|
1229
|
+
stdin.setEncoding("utf8");
|
|
1230
|
+
if (!listenerAdded) {
|
|
1231
|
+
stdin.on("data", function(key) {
|
|
1232
|
+
if (key === "") {
|
|
1233
|
+
process3.exit();
|
|
1234
|
+
}
|
|
1235
|
+
inputs.shift();
|
|
1236
|
+
inputs.push(key);
|
|
1237
|
+
const currentInput = inputs.join("");
|
|
1238
|
+
const targetListener = targetInputs[currentInput.toUpperCase()];
|
|
1239
|
+
if (targetListener && targetListenerConformsToCase(targetListener, currentInput)) {
|
|
1240
|
+
targetListener.closure(currentInput);
|
|
1241
|
+
inputs.fill(void 0);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
listenerAdded = true;
|
|
1245
|
+
}
|
|
1246
|
+
if (inputString.length > inputs.length) {
|
|
1247
|
+
inputs.length = inputString.length;
|
|
1248
|
+
}
|
|
1249
|
+
targetInputs[inputString.toUpperCase()] = Object.assign(options, { closure });
|
|
1250
|
+
}
|
|
1251
|
+
function targetListenerConformsToCase(targetListener, inputString) {
|
|
1252
|
+
if (targetListener.caseSensitive) {
|
|
1253
|
+
return inputString === inputString.toUpperCase();
|
|
1254
|
+
}
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
var stdin, targetInputs, inputs, listenerAdded;
|
|
1258
|
+
var init_listen_to_keyboard_key = __esm({
|
|
1259
|
+
"lib/utils/listen-to-keyboard-key.ts"() {
|
|
1260
|
+
stdin = process3.stdin;
|
|
1261
|
+
targetInputs = {};
|
|
1262
|
+
inputs = [];
|
|
1263
|
+
listenerAdded = false;
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// lib/setup/keyboard-events.ts
|
|
1268
|
+
function setupKeyboardEvents(config, cachedContent, connections) {
|
|
1269
|
+
listenToKeyboardKey("qq", () => abortBrowserQUnit(config, connections));
|
|
1270
|
+
listenToKeyboardKey("qa", () => {
|
|
1271
|
+
abortBrowserQUnit(config, connections);
|
|
1272
|
+
runTestsInBrowser(config, cachedContent, connections);
|
|
1273
|
+
});
|
|
1274
|
+
listenToKeyboardKey("qf", () => {
|
|
1275
|
+
abortBrowserQUnit(config, connections);
|
|
1276
|
+
if (!config.lastFailedTestFiles) {
|
|
1277
|
+
console.log("#", blue(`QUnitX: No tests failed so far, so repeating the last test run`));
|
|
1278
|
+
return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
|
|
1279
|
+
}
|
|
1280
|
+
runTestsInBrowser(config, cachedContent, connections, config.lastFailedTestFiles);
|
|
1281
|
+
});
|
|
1282
|
+
listenToKeyboardKey("ql", () => {
|
|
1283
|
+
abortBrowserQUnit(config, connections);
|
|
1284
|
+
runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function abortBrowserQUnit(_config, connections) {
|
|
1288
|
+
connections.server.publish("abort", "abort");
|
|
1289
|
+
}
|
|
1290
|
+
var init_keyboard_events = __esm({
|
|
1291
|
+
"lib/setup/keyboard-events.ts"() {
|
|
1292
|
+
init_color();
|
|
1293
|
+
init_listen_to_keyboard_key();
|
|
1294
|
+
init_tests_in_browser();
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// lib/setup/write-output-static-files.ts
|
|
1299
|
+
import fs10 from "node:fs/promises";
|
|
1300
|
+
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
1301
|
+
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
1302
|
+
const htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, "");
|
|
1303
|
+
await ensureFolderExists(`${projectRoot}/${output}/${htmlRelativePath}`);
|
|
1304
|
+
await fs10.writeFile(
|
|
1305
|
+
`${projectRoot}/${output}/${htmlRelativePath}`,
|
|
1306
|
+
cachedContent.staticHTMLs[staticHTMLKey]
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
1310
|
+
const assetRelativePath = assetAbsolutePath.replace(`${projectRoot}/`, "");
|
|
1311
|
+
await ensureFolderExists(`${projectRoot}/${output}/${assetRelativePath}`);
|
|
1312
|
+
await fs10.copyFile(assetAbsolutePath, `${projectRoot}/${output}/${assetRelativePath}`);
|
|
1313
|
+
});
|
|
1314
|
+
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
1315
|
+
}
|
|
1316
|
+
async function ensureFolderExists(assetPath) {
|
|
1317
|
+
await fs10.mkdir(assetPath.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
1318
|
+
}
|
|
1319
|
+
var init_write_output_static_files = __esm({
|
|
1320
|
+
"lib/setup/write-output-static-files.ts"() {
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// lib/commands/run.ts
|
|
1325
|
+
var run_exports = {};
|
|
1326
|
+
__export(run_exports, {
|
|
1327
|
+
default: () => run
|
|
1328
|
+
});
|
|
1329
|
+
import fs11 from "node:fs/promises";
|
|
1330
|
+
import { normalize } from "node:path";
|
|
1331
|
+
import { availableParallelism } from "node:os";
|
|
1332
|
+
async function run(config) {
|
|
1333
|
+
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
1334
|
+
if (config.watch) {
|
|
1335
|
+
const [connections] = await Promise.all([
|
|
1336
|
+
setupBrowser(config, cachedContent),
|
|
1337
|
+
writeOutputStaticFiles(config, cachedContent)
|
|
1338
|
+
]);
|
|
1339
|
+
config.expressApp = connections.server;
|
|
1340
|
+
setupKeyboardEvents(config, cachedContent, connections);
|
|
1341
|
+
if (config.before) {
|
|
1342
|
+
await runUserModule(`${process.cwd()}/${config.before}`, config, "before");
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
await runTestsInBrowser(config, cachedContent, connections);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
await Promise.all([
|
|
1348
|
+
connections.server && connections.server.close(),
|
|
1349
|
+
connections.browser && connections.browser.close()
|
|
1350
|
+
]);
|
|
1351
|
+
throw error;
|
|
1352
|
+
}
|
|
1353
|
+
logWatcherAndKeyboardShortcutInfo(config, connections.server);
|
|
1354
|
+
await setupFileWatchers(
|
|
1355
|
+
config.testFileLookupPaths,
|
|
1356
|
+
config,
|
|
1357
|
+
async (event, file) => {
|
|
1358
|
+
if (event === "addDir") return;
|
|
1359
|
+
if (["unlink", "unlinkDir"].includes(event)) {
|
|
1360
|
+
return await runTestsInBrowser(config, cachedContent, connections);
|
|
1361
|
+
}
|
|
1362
|
+
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
1363
|
+
},
|
|
1364
|
+
(_path, _event) => connections.server.publish("refresh", "refresh")
|
|
1365
|
+
);
|
|
1366
|
+
} else {
|
|
1367
|
+
const allFiles = Object.keys(config.fsTree);
|
|
1368
|
+
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
1369
|
+
const groups = splitIntoGroups(allFiles, groupCount);
|
|
1370
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
|
|
1371
|
+
config.lastRanTestFiles = allFiles;
|
|
1372
|
+
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
1373
|
+
...config,
|
|
1374
|
+
fsTree: Object.fromEntries(groupFiles.map((f) => [f, config.fsTree[f]])),
|
|
1375
|
+
// Single group keeps the root output dir for backward-compatible file paths.
|
|
1376
|
+
output: groupCount === 1 ? config.output : `${config.output}/group-${i}`,
|
|
1377
|
+
_groupMode: true
|
|
1378
|
+
}));
|
|
1379
|
+
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
1380
|
+
console.log("TAP version 13");
|
|
1381
|
+
const [browser] = await Promise.all([
|
|
1382
|
+
launchBrowser(config),
|
|
1383
|
+
Promise.all(
|
|
1384
|
+
groupConfigs.map(
|
|
1385
|
+
(groupConfig, i) => Promise.all([
|
|
1386
|
+
buildTestBundle(groupConfig, groupCachedContents[i]),
|
|
1387
|
+
writeOutputStaticFiles(groupConfig, groupCachedContents[i])
|
|
1388
|
+
])
|
|
1389
|
+
)
|
|
1390
|
+
)
|
|
1391
|
+
]);
|
|
1392
|
+
const TIME_COUNTER = timeCounter();
|
|
1393
|
+
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
1394
|
+
const keepAlive = setInterval(() => {
|
|
1395
|
+
}, 1e3);
|
|
1396
|
+
const groupResults = await Promise.allSettled(
|
|
1397
|
+
groupConfigs.map((groupConfig, i) => {
|
|
1398
|
+
const groupTimeout = new Promise((_, reject) => {
|
|
1399
|
+
const t = setTimeout(
|
|
1400
|
+
() => reject(new Error(`Group ${i} timed out after ${GROUP_TIMEOUT_MS}ms`)),
|
|
1401
|
+
GROUP_TIMEOUT_MS
|
|
1402
|
+
);
|
|
1403
|
+
t.unref();
|
|
1404
|
+
});
|
|
1405
|
+
return Promise.race([
|
|
1406
|
+
(async () => {
|
|
1407
|
+
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
1408
|
+
groupConfig.expressApp = connections.server;
|
|
1409
|
+
if (config.before) {
|
|
1410
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
1414
|
+
} finally {
|
|
1415
|
+
await Promise.all([
|
|
1416
|
+
connections.server && connections.server.close(),
|
|
1417
|
+
connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
1418
|
+
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
1419
|
+
Promise.race([
|
|
1420
|
+
connections.page.close(),
|
|
1421
|
+
new Promise((resolve) => {
|
|
1422
|
+
const t = setTimeout(resolve, 1e4);
|
|
1423
|
+
t.unref();
|
|
1424
|
+
})
|
|
1425
|
+
]).catch(() => {
|
|
1426
|
+
})
|
|
1427
|
+
]);
|
|
1428
|
+
}
|
|
1429
|
+
})(),
|
|
1430
|
+
groupTimeout
|
|
1431
|
+
]);
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
const exitCode = groupResults.reduce(
|
|
1435
|
+
(code, { status, reason }) => {
|
|
1436
|
+
if (status !== "rejected") return code;
|
|
1437
|
+
console.error(reason);
|
|
1438
|
+
return 1;
|
|
1439
|
+
},
|
|
1440
|
+
config.COUNTER.failCount > 0 ? 1 : 0
|
|
1441
|
+
);
|
|
1442
|
+
process.exitCode = exitCode;
|
|
1443
|
+
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
1444
|
+
if (config.after) {
|
|
1445
|
+
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
1446
|
+
}
|
|
1447
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), 5e3);
|
|
1448
|
+
exitTimer.unref();
|
|
1449
|
+
process.stdout.write("\n", () => {
|
|
1450
|
+
clearTimeout(exitTimer);
|
|
1451
|
+
clearInterval(keepAlive);
|
|
1452
|
+
browser.close().catch(() => {
|
|
1453
|
+
});
|
|
1454
|
+
process.exit(exitCode);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async function buildCachedContent(config, htmlPaths) {
|
|
1459
|
+
const htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs11.readFile(htmlPath)));
|
|
1460
|
+
const cachedContent = htmlPaths.reduce(
|
|
1461
|
+
(result, _htmlPath, index) => {
|
|
1462
|
+
const filePath = config.htmlPaths[index];
|
|
1463
|
+
const html = htmlBuffers[index].toString();
|
|
1464
|
+
if (html.includes("{{content}}")) {
|
|
1465
|
+
result.dynamicContentHTMLs[filePath] = html;
|
|
1466
|
+
result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
|
|
1467
|
+
} else {
|
|
1468
|
+
console.log(
|
|
1469
|
+
"#",
|
|
1470
|
+
yellow(
|
|
1471
|
+
`WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`
|
|
1472
|
+
)
|
|
1473
|
+
);
|
|
1474
|
+
result.staticHTMLs[filePath] = html;
|
|
1475
|
+
}
|
|
1476
|
+
findInternalAssetsFromHTML(html).forEach((key) => {
|
|
1477
|
+
result.assets.add(normalizeInternalAssetPathFromHTML(config.projectRoot, key, filePath));
|
|
1478
|
+
});
|
|
1479
|
+
return result;
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
allTestCode: null,
|
|
1483
|
+
assets: /* @__PURE__ */ new Set(),
|
|
1484
|
+
htmlPathsToRunTests: [],
|
|
1485
|
+
mainHTML: { filePath: null, html: null },
|
|
1486
|
+
staticHTMLs: {},
|
|
1487
|
+
dynamicContentHTMLs: {}
|
|
1488
|
+
}
|
|
1489
|
+
);
|
|
1490
|
+
if (cachedContent.htmlPathsToRunTests.length === 0) {
|
|
1491
|
+
cachedContent.htmlPathsToRunTests = ["/"];
|
|
1492
|
+
}
|
|
1493
|
+
return addCachedContentMainHTML(config.projectRoot, cachedContent);
|
|
1494
|
+
}
|
|
1495
|
+
async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
1496
|
+
const mainHTMLPath = Object.keys(cachedContent.dynamicContentHTMLs)[0];
|
|
1497
|
+
if (mainHTMLPath) {
|
|
1498
|
+
cachedContent.mainHTML = {
|
|
1499
|
+
filePath: mainHTMLPath,
|
|
1500
|
+
html: cachedContent.dynamicContentHTMLs[mainHTMLPath]
|
|
1501
|
+
};
|
|
1502
|
+
} else {
|
|
1503
|
+
const html = await readBoilerplate("setup/tests.hbs");
|
|
1504
|
+
cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
|
|
1505
|
+
cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
|
|
1506
|
+
}
|
|
1507
|
+
return cachedContent;
|
|
1508
|
+
}
|
|
1509
|
+
function splitIntoGroups(files, groupCount) {
|
|
1510
|
+
const groups = Array.from({ length: groupCount }, () => []);
|
|
1511
|
+
files.forEach((file, i) => groups[i % groupCount].push(file));
|
|
1512
|
+
return groups.filter((g) => g.length > 0);
|
|
1513
|
+
}
|
|
1514
|
+
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
1515
|
+
console.log(
|
|
1516
|
+
"#",
|
|
1517
|
+
blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`)
|
|
1518
|
+
);
|
|
1519
|
+
console.log(
|
|
1520
|
+
"#",
|
|
1521
|
+
blue(
|
|
1522
|
+
`Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`
|
|
1523
|
+
)
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
1527
|
+
const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
|
|
1528
|
+
return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
|
|
1529
|
+
}
|
|
1530
|
+
var init_run = __esm({
|
|
1531
|
+
"lib/commands/run.ts"() {
|
|
1532
|
+
init_browser();
|
|
1533
|
+
init_color();
|
|
1534
|
+
init_tests_in_browser();
|
|
1535
|
+
init_file_watcher();
|
|
1536
|
+
init_find_internal_assets_from_html();
|
|
1537
|
+
init_run_user_module();
|
|
1538
|
+
init_keyboard_events();
|
|
1539
|
+
init_write_output_static_files();
|
|
1540
|
+
init_time_counter();
|
|
1541
|
+
init_display_final_result();
|
|
1542
|
+
init_read_boilerplate();
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
// cli.ts
|
|
1547
|
+
init_early_chrome();
|
|
1548
|
+
import process4 from "node:process";
|
|
1549
|
+
|
|
1550
|
+
// lib/commands/help.ts
|
|
1551
|
+
init_color();
|
|
1552
|
+
|
|
1553
|
+
// package.json
|
|
1554
|
+
var package_default = {
|
|
1555
|
+
name: "qunitx-cli",
|
|
1556
|
+
type: "module",
|
|
1557
|
+
version: "0.9.7",
|
|
1558
|
+
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
1559
|
+
author: "Izel Nakri",
|
|
1560
|
+
license: "MIT",
|
|
1561
|
+
keywords: [
|
|
1562
|
+
"test runner",
|
|
1563
|
+
"testing",
|
|
1564
|
+
"browser",
|
|
1565
|
+
"ci",
|
|
1566
|
+
"qunit",
|
|
1567
|
+
"qunitx"
|
|
1568
|
+
],
|
|
1569
|
+
files: [
|
|
1570
|
+
"bin/",
|
|
1571
|
+
"dist/",
|
|
1572
|
+
"templates/"
|
|
1573
|
+
],
|
|
1574
|
+
scripts: {
|
|
1575
|
+
build: "node scripts/build-cli.js",
|
|
1576
|
+
bin: "chmod +x cli.ts && ./cli.ts",
|
|
1577
|
+
prepublishOnly: "npm run build",
|
|
1578
|
+
format: 'prettier --check "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
|
|
1579
|
+
"format:fix": 'prettier --write "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
|
|
1580
|
+
lint: "deno lint lib/ cli.ts",
|
|
1581
|
+
"lint:docs": "node scripts/lint-docs.js",
|
|
1582
|
+
docs: `deno doc --html --name="qunitx-cli" --output=docs/lib 'lib/**/*.ts' README.md`,
|
|
1583
|
+
"changelog:unreleased": "git-cliff --unreleased --strip all",
|
|
1584
|
+
"changelog:preview": "git-cliff",
|
|
1585
|
+
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
1586
|
+
postinstall: "deno install --allow-scripts=npm:playwright-core || true",
|
|
1587
|
+
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`,
|
|
1588
|
+
"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`,
|
|
1589
|
+
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
1590
|
+
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|
|
1591
|
+
},
|
|
1592
|
+
engines: {
|
|
1593
|
+
node: ">=24.0.0",
|
|
1594
|
+
deno: ">=2.7.0"
|
|
1595
|
+
},
|
|
1596
|
+
bin: {
|
|
1597
|
+
qunitx: "bin/qunitx.js"
|
|
1598
|
+
},
|
|
1599
|
+
repository: {
|
|
1600
|
+
type: "git",
|
|
1601
|
+
url: "https://github.com/izelnakri/qunitx-cli.git"
|
|
1602
|
+
},
|
|
1603
|
+
dependencies: {
|
|
1604
|
+
esbuild: "^0.27.3",
|
|
1605
|
+
"playwright-core": "^1.58.2",
|
|
1606
|
+
ws: "^8.20.0"
|
|
1607
|
+
},
|
|
1608
|
+
devDependencies: {
|
|
1609
|
+
cors: "^2.8.6",
|
|
1610
|
+
express: "^5.2.1",
|
|
1611
|
+
"js-yaml": "^4.1.1",
|
|
1612
|
+
prettier: "^3.8.1",
|
|
1613
|
+
qunitx: "^1.1.2",
|
|
1614
|
+
typescript: "^6.0.2"
|
|
1615
|
+
},
|
|
1616
|
+
volta: {
|
|
1617
|
+
node: "24.14.0"
|
|
1618
|
+
},
|
|
1619
|
+
prettier: {
|
|
1620
|
+
printWidth: 100,
|
|
1621
|
+
singleQuote: true,
|
|
1622
|
+
arrowParens: "always"
|
|
1623
|
+
},
|
|
1624
|
+
optionalDependencies: {
|
|
1625
|
+
"qunitx-cli-linux-x64": "*"
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
// lib/commands/help.ts
|
|
1630
|
+
var highlight = (text) => magenta().bold(text);
|
|
1631
|
+
var color = (text) => blue(text);
|
|
1632
|
+
function displayHelpOutput() {
|
|
1633
|
+
const config = package_default;
|
|
1634
|
+
console.log(`${highlight("[qunitx v" + config.version + "] Usage:")} qunitx ${color("[targets] --$flags")}
|
|
1635
|
+
|
|
1636
|
+
${highlight("Input options:")}
|
|
1637
|
+
- File: $ ${color("qunitx test/foo.js")}
|
|
1638
|
+
- Folder: $ ${color("qunitx test/login")}
|
|
1639
|
+
- Globs: $ ${color("qunitx test/**/*-test.js")}
|
|
1640
|
+
- Combination: $ ${color("qunitx test/foo.js test/bar.js test/*-test.js test/logout")}
|
|
1641
|
+
|
|
1642
|
+
${highlight("Optional flags:")}
|
|
1643
|
+
${color("--debug")} : print console output when tests run in browser
|
|
1644
|
+
${color("--watch")} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
1645
|
+
${color("--timeout")} : change default timeout per test case
|
|
1646
|
+
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
1647
|
+
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
1648
|
+
${color("--port")} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
1649
|
+
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts]
|
|
1650
|
+
${color("--browser")} : browser engine to run tests in: chromium, firefox, webkit[default: chromium]
|
|
1651
|
+
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
1652
|
+
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
1653
|
+
|
|
1654
|
+
${highlight("Example:")} $ ${color("qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js")}
|
|
1655
|
+
|
|
1656
|
+
${highlight("Commands:")}
|
|
1657
|
+
${color("$ qunitx init")} # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
1658
|
+
${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
1659
|
+
`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// lib/commands/init.ts
|
|
1663
|
+
import fs3 from "node:fs/promises";
|
|
1664
|
+
import path from "node:path";
|
|
1665
|
+
|
|
1666
|
+
// lib/utils/find-project-root.ts
|
|
1667
|
+
import process2 from "node:process";
|
|
1668
|
+
|
|
1669
|
+
// lib/utils/search-in-parent-directories.ts
|
|
1670
|
+
init_path_exists();
|
|
1671
|
+
async function searchInParentDirectories(directory, targetEntry) {
|
|
1672
|
+
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
1673
|
+
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
1674
|
+
return `${resolvedDirectory}/${targetEntry}`;
|
|
1675
|
+
} else if (resolvedDirectory === "") {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
return await searchInParentDirectories(
|
|
1679
|
+
resolvedDirectory.slice(0, resolvedDirectory.lastIndexOf("/")),
|
|
1680
|
+
targetEntry
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
var search_in_parent_directories_default = searchInParentDirectories;
|
|
1684
|
+
|
|
1685
|
+
// lib/utils/find-project-root.ts
|
|
1686
|
+
async function findProjectRoot() {
|
|
1687
|
+
try {
|
|
1688
|
+
const absolutePath = await search_in_parent_directories_default(".", "package.json");
|
|
1689
|
+
if (!absolutePath.includes("package.json")) {
|
|
1690
|
+
throw new Error("package.json mising");
|
|
1691
|
+
}
|
|
1692
|
+
return absolutePath.replace("/package.json", "");
|
|
1693
|
+
} catch (_error) {
|
|
1694
|
+
console.log("couldnt find projects package.json, did you run $ npm init ??");
|
|
1695
|
+
process2.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// lib/commands/init.ts
|
|
1700
|
+
init_path_exists();
|
|
1701
|
+
|
|
1702
|
+
// lib/setup/default-project-config-values.ts
|
|
1703
|
+
var default_project_config_values_default = {
|
|
1704
|
+
output: "tmp",
|
|
1705
|
+
timeout: 2e4,
|
|
1706
|
+
failFast: false,
|
|
1707
|
+
port: 1234,
|
|
1708
|
+
extensions: ["js", "ts"],
|
|
1709
|
+
browser: "chromium"
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
// lib/commands/init.ts
|
|
1713
|
+
init_read_boilerplate();
|
|
1714
|
+
async function initializeProject() {
|
|
1715
|
+
const projectRoot = await findProjectRoot();
|
|
1716
|
+
const oldPackageJSON = JSON.parse(await fs3.readFile(`${projectRoot}/package.json`));
|
|
1717
|
+
const existingQunitx = oldPackageJSON.qunitx || {};
|
|
1718
|
+
const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
|
|
1719
|
+
const config = Object.assign({}, default_project_config_values_default, existingQunitx, {
|
|
1720
|
+
htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
|
|
1721
|
+
});
|
|
1722
|
+
await Promise.all([
|
|
1723
|
+
writeTestsHTML(projectRoot, config, oldPackageJSON),
|
|
1724
|
+
rewritePackageJSON(projectRoot, config, oldPackageJSON),
|
|
1725
|
+
writeTSConfigIfNeeded(projectRoot)
|
|
1726
|
+
]);
|
|
1727
|
+
}
|
|
1728
|
+
async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
1729
|
+
const testHTMLTemplateBuffer = await readBoilerplate("setup/tests.hbs");
|
|
1730
|
+
return await Promise.all(
|
|
1731
|
+
config.htmlPaths.map(async (htmlPath) => {
|
|
1732
|
+
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
1733
|
+
if (await pathExists(targetPath)) {
|
|
1734
|
+
return console.log(`${htmlPath} already exists`);
|
|
1735
|
+
} else {
|
|
1736
|
+
const targetDirectory = path.dirname(targetPath);
|
|
1737
|
+
const _targetOutputPath = path.relative(
|
|
1738
|
+
targetDirectory,
|
|
1739
|
+
`${projectRoot}/${config.output}/tests.js`
|
|
1740
|
+
);
|
|
1741
|
+
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
1742
|
+
"{{applicationName}}",
|
|
1743
|
+
oldPackageJSON.name
|
|
1744
|
+
);
|
|
1745
|
+
await fs3.mkdir(targetDirectory, { recursive: true });
|
|
1746
|
+
await fs3.writeFile(targetPath, testHTMLTemplate);
|
|
1747
|
+
console.log(`${targetPath} written`);
|
|
1748
|
+
}
|
|
1749
|
+
})
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
|
|
1753
|
+
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
1754
|
+
await fs3.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
1755
|
+
}
|
|
1756
|
+
async function writeTSConfigIfNeeded(projectRoot) {
|
|
1757
|
+
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
1758
|
+
if (!await pathExists(targetPath)) {
|
|
1759
|
+
const tsConfigTemplate = await readBoilerplate("setup/tsconfig.json");
|
|
1760
|
+
await fs3.writeFile(targetPath, tsConfigTemplate);
|
|
1761
|
+
console.log(`${targetPath} written`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// lib/commands/generate.ts
|
|
1766
|
+
init_color();
|
|
1767
|
+
import fs4 from "node:fs/promises";
|
|
1768
|
+
init_path_exists();
|
|
1769
|
+
init_read_boilerplate();
|
|
1770
|
+
async function generateTestFiles() {
|
|
1771
|
+
const projectRoot = await findProjectRoot();
|
|
1772
|
+
const moduleName = process.argv[3];
|
|
1773
|
+
const path5 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
1774
|
+
if (await pathExists(path5)) {
|
|
1775
|
+
console.log(`${path5} already exists!`);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const testJSContent = await readBoilerplate("test.js");
|
|
1779
|
+
const targetFolderPaths = path5.split("/");
|
|
1780
|
+
targetFolderPaths.pop();
|
|
1781
|
+
await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
1782
|
+
await fs4.writeFile(path5, testJSContent.replace("{{moduleName}}", moduleName));
|
|
1783
|
+
console.log(green(`${path5} written`));
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// lib/setup/config.ts
|
|
1787
|
+
import fs6 from "node:fs/promises";
|
|
1788
|
+
|
|
1789
|
+
// lib/setup/fs-tree.ts
|
|
1790
|
+
import fs5, { glob as fsGlob } from "node:fs/promises";
|
|
1791
|
+
import path2 from "node:path";
|
|
1792
|
+
function isGlob(str) {
|
|
1793
|
+
return /[*?{[]/.test(str);
|
|
1794
|
+
}
|
|
1795
|
+
async function readDirRecursive(dir, filter) {
|
|
1796
|
+
const entries = await fs5.readdir(dir, { recursive: true, withFileTypes: true });
|
|
1797
|
+
return entries.filter((e) => e.isFile() && filter(e.name)).map((e) => path2.join(e.parentPath, e.name));
|
|
1798
|
+
}
|
|
1799
|
+
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
1800
|
+
const targetExtensions = config.extensions || ["js", "ts"];
|
|
1801
|
+
const fsTree = {};
|
|
1802
|
+
await Promise.all(
|
|
1803
|
+
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
1804
|
+
try {
|
|
1805
|
+
if (isGlob(fileAbsolutePath)) {
|
|
1806
|
+
for await (const fileName of fsGlob(fileAbsolutePath)) {
|
|
1807
|
+
if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
|
|
1808
|
+
fsTree[fileName] = null;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
} else {
|
|
1812
|
+
const entry = await fs5.stat(fileAbsolutePath);
|
|
1813
|
+
if (entry.isFile()) {
|
|
1814
|
+
fsTree[fileAbsolutePath] = null;
|
|
1815
|
+
} else if (entry.isDirectory()) {
|
|
1816
|
+
const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
|
|
1817
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
1818
|
+
});
|
|
1819
|
+
fileNames.forEach((fileName) => {
|
|
1820
|
+
fsTree[fileName] = null;
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
console.error(error);
|
|
1826
|
+
return process.exit(1);
|
|
1827
|
+
}
|
|
1828
|
+
})
|
|
1829
|
+
);
|
|
1830
|
+
return fsTree;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// lib/setup/test-file-paths.ts
|
|
1834
|
+
import { matchesGlob } from "node:path";
|
|
1835
|
+
function isGlob2(str) {
|
|
1836
|
+
return /[*?{[]/.test(str);
|
|
1837
|
+
}
|
|
1838
|
+
function setupTestFilePaths(_projectRoot, inputs2) {
|
|
1839
|
+
const [folders, filesWithGlob, filesWithoutGlob] = inputs2.reduce(
|
|
1840
|
+
(result2, input) => {
|
|
1841
|
+
const glob = isGlob2(input);
|
|
1842
|
+
if (!pathIsFile(input)) {
|
|
1843
|
+
result2[0].push({ input, isFile: false, isGlob: glob });
|
|
1844
|
+
} else {
|
|
1845
|
+
result2[glob ? 1 : 2].push({ input, isFile: true, isGlob: glob });
|
|
1846
|
+
}
|
|
1847
|
+
return result2;
|
|
1848
|
+
},
|
|
1849
|
+
[[], [], []]
|
|
1850
|
+
);
|
|
1851
|
+
const result = folders.reduce((folderResult, folder) => {
|
|
1852
|
+
if (!pathIsIncludedInPaths(folders, folder)) {
|
|
1853
|
+
folderResult.push(folder);
|
|
1854
|
+
}
|
|
1855
|
+
return folderResult;
|
|
1856
|
+
}, []);
|
|
1857
|
+
filesWithGlob.forEach((file) => {
|
|
1858
|
+
if (!pathIsIncludedInPaths(result, file) && !pathIsIncludedInPaths(filesWithGlob, file)) {
|
|
1859
|
+
result.push(file);
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
filesWithoutGlob.forEach((file) => {
|
|
1863
|
+
if (!pathIsIncludedInPaths(result, file)) {
|
|
1864
|
+
result.push(file);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
return result.map((metaItem) => metaItem.input);
|
|
1868
|
+
}
|
|
1869
|
+
function pathIsFile(path5) {
|
|
1870
|
+
const inputs2 = path5.split("/");
|
|
1871
|
+
return inputs2[inputs2.length - 1].includes(".");
|
|
1872
|
+
}
|
|
1873
|
+
function pathIsIncludedInPaths(paths, targetPath) {
|
|
1874
|
+
return paths.some((path5) => {
|
|
1875
|
+
if (path5 === targetPath) {
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path5));
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
function buildGlobFormat(path5) {
|
|
1882
|
+
return path5.isFile ? path5.input : `${path5.input}/**`;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// lib/utils/parse-cli-flags.ts
|
|
1886
|
+
function parseCliFlags(projectRoot) {
|
|
1887
|
+
const providedFlags = process.argv.slice(2).reduce(
|
|
1888
|
+
(result, arg) => {
|
|
1889
|
+
if (arg.startsWith("--debug")) {
|
|
1890
|
+
return Object.assign(result, { debug: parseBoolean(arg.split("=")[1]) });
|
|
1891
|
+
} else if (arg.startsWith("--watch")) {
|
|
1892
|
+
return Object.assign(result, { watch: parseBoolean(arg.split("=")[1]) });
|
|
1893
|
+
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
1894
|
+
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
1895
|
+
} else if (arg.startsWith("--timeout")) {
|
|
1896
|
+
return Object.assign(result, { timeout: Number(arg.split("=")[1]) || 1e4 });
|
|
1897
|
+
} else if (arg.startsWith("--output")) {
|
|
1898
|
+
return Object.assign(result, { output: arg.split("=")[1] });
|
|
1899
|
+
} else if (arg.endsWith(".html")) {
|
|
1900
|
+
if (result.htmlPaths) {
|
|
1901
|
+
result.htmlPaths.push(arg);
|
|
1902
|
+
} else {
|
|
1903
|
+
result.htmlPaths = [arg];
|
|
1904
|
+
}
|
|
1905
|
+
return result;
|
|
1906
|
+
} else if (arg.startsWith("--port")) {
|
|
1907
|
+
return Object.assign(result, { port: Number(arg.split("=")[1]) });
|
|
1908
|
+
} else if (arg.startsWith("--extensions")) {
|
|
1909
|
+
return Object.assign(result, {
|
|
1910
|
+
extensions: arg.split("=")[1].split(",").map((e) => e.trim())
|
|
1911
|
+
});
|
|
1912
|
+
} else if (arg.startsWith("--browser")) {
|
|
1913
|
+
const value = arg.split("=")[1];
|
|
1914
|
+
if (!["chromium", "firefox", "webkit"].includes(value)) {
|
|
1915
|
+
console.error(
|
|
1916
|
+
`Invalid --browser value: "${value}". Must be one of: chromium, firefox, webkit`
|
|
1917
|
+
);
|
|
1918
|
+
process.exit(1);
|
|
1919
|
+
}
|
|
1920
|
+
return Object.assign(result, { browser: value });
|
|
1921
|
+
} else if (arg.startsWith("--before")) {
|
|
1922
|
+
return Object.assign(result, { before: parseModule(arg.split("=")[1]) });
|
|
1923
|
+
} else if (arg.startsWith("--after")) {
|
|
1924
|
+
return Object.assign(result, { after: parseModule(arg.split("=")[1]) });
|
|
1925
|
+
} else if (arg === "--trace-perf") {
|
|
1926
|
+
return result;
|
|
1927
|
+
}
|
|
1928
|
+
result.inputs.add(arg.startsWith(projectRoot) ? arg : `${process.cwd()}/${arg}`);
|
|
1929
|
+
return result;
|
|
1930
|
+
},
|
|
1931
|
+
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
1932
|
+
);
|
|
1933
|
+
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
1934
|
+
}
|
|
1935
|
+
function parseBoolean(result, defaultValue = true) {
|
|
1936
|
+
if (result === "true") {
|
|
1937
|
+
return true;
|
|
1938
|
+
} else if (result === "false") {
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
return defaultValue;
|
|
1942
|
+
}
|
|
1943
|
+
function parseModule(value) {
|
|
1944
|
+
if (["false", "'false'", '"false"', ""].includes(value)) {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
return value;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// lib/setup/config.ts
|
|
1951
|
+
async function setupConfig() {
|
|
1952
|
+
const projectRoot = await findProjectRoot();
|
|
1953
|
+
const cliConfigFlags = parseCliFlags(projectRoot);
|
|
1954
|
+
const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
|
|
1955
|
+
const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
|
|
1956
|
+
const config = {
|
|
1957
|
+
...default_project_config_values_default,
|
|
1958
|
+
htmlPaths: [],
|
|
1959
|
+
...projectPackageJSON.qunitx || {},
|
|
1960
|
+
...cliConfigFlags,
|
|
1961
|
+
projectRoot,
|
|
1962
|
+
inputs: inputs2,
|
|
1963
|
+
testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
|
|
1964
|
+
lastFailedTestFiles: null,
|
|
1965
|
+
lastRanTestFiles: null,
|
|
1966
|
+
COUNTER: { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 },
|
|
1967
|
+
_testRunDone: null,
|
|
1968
|
+
_resetTestTimeout: null
|
|
1969
|
+
};
|
|
1970
|
+
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
1971
|
+
config.fsTree = await buildFSTree(config.testFileLookupPaths, config);
|
|
1972
|
+
return config;
|
|
1973
|
+
}
|
|
1974
|
+
async function readConfigFromPackageJSON(projectRoot) {
|
|
1975
|
+
const packageJSON = await fs6.readFile(`${projectRoot}/package.json`);
|
|
1976
|
+
return JSON.parse(packageJSON.toString());
|
|
1977
|
+
}
|
|
1978
|
+
function normalizeHTMLPaths(projectRoot, htmlPaths) {
|
|
1979
|
+
return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
|
|
1980
|
+
}
|
|
1981
|
+
function readInputsFromPackageJSON(packageJSON) {
|
|
1982
|
+
const qunitx = packageJSON.qunitx;
|
|
1983
|
+
return qunitx && qunitx.inputs ? qunitx.inputs : [];
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// cli.ts
|
|
1987
|
+
process4.title = "qunitx";
|
|
1988
|
+
(async () => {
|
|
1989
|
+
if (!process4.argv[2]) {
|
|
1990
|
+
return await displayHelpOutput();
|
|
1991
|
+
} else if (["help", "h", "p", "print"].includes(process4.argv[2])) {
|
|
1992
|
+
return await displayHelpOutput();
|
|
1993
|
+
} else if (["new", "n", "g", "generate"].includes(process4.argv[2])) {
|
|
1994
|
+
return await generateTestFiles();
|
|
1995
|
+
} else if (["init"].includes(process4.argv[2])) {
|
|
1996
|
+
return await initializeProject();
|
|
1997
|
+
}
|
|
1998
|
+
const [config, { default: run2 }] = await Promise.all([
|
|
1999
|
+
setupConfig(),
|
|
2000
|
+
Promise.resolve().then(() => (init_run(), run_exports))
|
|
2001
|
+
]);
|
|
2002
|
+
try {
|
|
2003
|
+
return await run2(config);
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
console.error(error);
|
|
2006
|
+
process4.exitCode = 1;
|
|
2007
|
+
process4.stdout.write("\n", () => process4.exit(1));
|
|
2008
|
+
}
|
|
2009
|
+
})();
|