@epikodelabs/testify 1.0.16
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/LICENSE +21 -0
- package/README.md +686 -0
- package/assets/favicon.ico +0 -0
- package/bin/jasmine +1053 -0
- package/bin/testify +4640 -0
- package/esm-loader.mjs +332 -0
- package/lib/index.js +231 -0
- package/package.json +89 -0
- package/postinstall.mjs +161 -0
package/bin/jasmine
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as ___createRequire } from "module";
|
|
3
|
+
const require2 = ___createRequire(import.meta.url);
|
|
4
|
+
const ___fileURLToPath = require2("url").fileURLToPath;
|
|
5
|
+
const ___path = require2("path");
|
|
6
|
+
const __filename = ___fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = ___path.dirname(__filename);
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import fs__default from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
13
|
+
import "util";
|
|
14
|
+
const scriptRel = "modulepreload";
|
|
15
|
+
const assetsURL = function(dep) {
|
|
16
|
+
return "/" + dep;
|
|
17
|
+
};
|
|
18
|
+
const seen = {};
|
|
19
|
+
const __vitePreload = function preload(baseModule, deps, importerUrl) {
|
|
20
|
+
let promise = Promise.resolve();
|
|
21
|
+
if (deps && deps.length > 0) {
|
|
22
|
+
let allSettled = function(promises$2) {
|
|
23
|
+
return Promise.all(promises$2.map((p) => Promise.resolve(p).then((value$1) => ({
|
|
24
|
+
status: "fulfilled",
|
|
25
|
+
value: value$1
|
|
26
|
+
}), (reason) => ({
|
|
27
|
+
status: "rejected",
|
|
28
|
+
reason
|
|
29
|
+
}))));
|
|
30
|
+
};
|
|
31
|
+
document.getElementsByTagName("link");
|
|
32
|
+
const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
|
|
33
|
+
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
|
|
34
|
+
promise = allSettled(deps.map((dep) => {
|
|
35
|
+
dep = assetsURL(dep);
|
|
36
|
+
if (dep in seen) return;
|
|
37
|
+
seen[dep] = true;
|
|
38
|
+
const isCss = dep.endsWith(".css");
|
|
39
|
+
const cssSelector = isCss ? '[rel="stylesheet"]' : "";
|
|
40
|
+
if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return;
|
|
41
|
+
const link = document.createElement("link");
|
|
42
|
+
link.rel = isCss ? "stylesheet" : scriptRel;
|
|
43
|
+
if (!isCss) link.as = "script";
|
|
44
|
+
link.crossOrigin = "";
|
|
45
|
+
link.href = dep;
|
|
46
|
+
if (cspNonce) link.setAttribute("nonce", cspNonce);
|
|
47
|
+
document.head.appendChild(link);
|
|
48
|
+
if (isCss) return new Promise((res, rej) => {
|
|
49
|
+
link.addEventListener("load", res);
|
|
50
|
+
link.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`)));
|
|
51
|
+
});
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function handlePreloadError(err$2) {
|
|
55
|
+
const e$1 = new Event("vite:preloadError", { cancelable: true });
|
|
56
|
+
e$1.payload = err$2;
|
|
57
|
+
window.dispatchEvent(e$1);
|
|
58
|
+
if (!e$1.defaultPrevented) throw err$2;
|
|
59
|
+
}
|
|
60
|
+
return promise.then((res) => {
|
|
61
|
+
for (const item of res || []) {
|
|
62
|
+
if (item.status !== "rejected") continue;
|
|
63
|
+
handlePreloadError(item.reason);
|
|
64
|
+
}
|
|
65
|
+
return baseModule().catch(handlePreloadError);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const MAX_WIDTH = 63;
|
|
69
|
+
const ANSI_FULL_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
70
|
+
function wrapLine(text, width, indentation = 0, mode = "char") {
|
|
71
|
+
text = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
72
|
+
const indent = " ".repeat(indentation);
|
|
73
|
+
const indentWidth = indent.length;
|
|
74
|
+
if (width <= indentWidth) width = indentWidth + 1;
|
|
75
|
+
const availableWidth = width - indentWidth;
|
|
76
|
+
return mode === "char" ? wrapByChar(text, availableWidth, indent) : wrapByWord(text, availableWidth, indent);
|
|
77
|
+
}
|
|
78
|
+
function wrapByChar(text, available, indent) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
let buf = "";
|
|
81
|
+
let vis = 0;
|
|
82
|
+
const tokens = text.split(ANSI_FULL_REGEX);
|
|
83
|
+
for (const token of tokens) {
|
|
84
|
+
if (ANSI_FULL_REGEX.test(token)) {
|
|
85
|
+
buf += token;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const ch of [...token]) {
|
|
89
|
+
if (vis >= available) {
|
|
90
|
+
lines.push(indent + buf);
|
|
91
|
+
buf = "";
|
|
92
|
+
vis = 0;
|
|
93
|
+
}
|
|
94
|
+
buf += ch;
|
|
95
|
+
vis++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (buf) lines.push(indent + buf);
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
function wrapByWord(text, available, indent) {
|
|
102
|
+
const lines = [];
|
|
103
|
+
let buf = "";
|
|
104
|
+
let vis = 0;
|
|
105
|
+
let word = "";
|
|
106
|
+
let wordVis = 0;
|
|
107
|
+
const flushWord = () => {
|
|
108
|
+
if (!word) return;
|
|
109
|
+
if (wordVis > available) {
|
|
110
|
+
for (const ch of [...word]) {
|
|
111
|
+
if (vis >= available) {
|
|
112
|
+
lines.push(indent + buf.trimEnd());
|
|
113
|
+
buf = "";
|
|
114
|
+
vis = 0;
|
|
115
|
+
}
|
|
116
|
+
buf += ch;
|
|
117
|
+
vis++;
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
if (vis + wordVis > available && vis > 0) {
|
|
121
|
+
lines.push(indent + buf.trimEnd());
|
|
122
|
+
buf = "";
|
|
123
|
+
vis = 0;
|
|
124
|
+
}
|
|
125
|
+
buf += word;
|
|
126
|
+
vis += wordVis;
|
|
127
|
+
}
|
|
128
|
+
word = "";
|
|
129
|
+
wordVis = 0;
|
|
130
|
+
};
|
|
131
|
+
for (const ch of [...text]) {
|
|
132
|
+
if (/\s/.test(ch)) {
|
|
133
|
+
flushWord();
|
|
134
|
+
if (vis < available && vis > 0) {
|
|
135
|
+
buf += " ";
|
|
136
|
+
vis++;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
word += ch;
|
|
140
|
+
wordVis++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
flushWord();
|
|
144
|
+
if (buf) lines.push(indent + buf.trimEnd());
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
const colors = {
|
|
148
|
+
reset: "\x1B[0m",
|
|
149
|
+
bold: "\x1B[1m",
|
|
150
|
+
brightRed: "\x1B[91m",
|
|
151
|
+
brightGreen: "\x1B[92m"
|
|
152
|
+
};
|
|
153
|
+
class Logger {
|
|
154
|
+
previousLines = [];
|
|
155
|
+
showPrompt = true;
|
|
156
|
+
prompt;
|
|
157
|
+
errorPrompt;
|
|
158
|
+
constructor(options = {}) {
|
|
159
|
+
this.prompt = `${colors.bold}${options.promptColor ?? colors.brightGreen}> ${colors.reset}`;
|
|
160
|
+
this.errorPrompt = `${options.errorPromptColor ?? colors.brightRed}> ${colors.reset}`;
|
|
161
|
+
}
|
|
162
|
+
// ─── Utilities ───────────────────────────────────────────
|
|
163
|
+
visibleWidth(str) {
|
|
164
|
+
return [...str.replace(ANSI_FULL_REGEX, "")].length;
|
|
165
|
+
}
|
|
166
|
+
clearLine() {
|
|
167
|
+
process.stdout.write("\r\x1B[K");
|
|
168
|
+
}
|
|
169
|
+
writeLine(line, color = "") {
|
|
170
|
+
this.clearLine();
|
|
171
|
+
process.stdout.write(color + line + colors.reset);
|
|
172
|
+
if (this.visibleWidth(line) >= MAX_WIDTH) {
|
|
173
|
+
process.stdout.write("\n");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
addLine(text, opts = {}) {
|
|
177
|
+
this.previousLines.push({
|
|
178
|
+
text,
|
|
179
|
+
isRaw: opts.isRaw,
|
|
180
|
+
hasPrompt: opts.hasPrompt ?? this.showPrompt
|
|
181
|
+
});
|
|
182
|
+
if (this.previousLines.length > 200) {
|
|
183
|
+
this.previousLines.splice(0, 100);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ─── REFORMAT (RESTORED) ─────────────────────────────────
|
|
187
|
+
reformat(text, opts) {
|
|
188
|
+
const { width, align = "left", padChar = " " } = opts;
|
|
189
|
+
const normalized = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
190
|
+
const lines = [];
|
|
191
|
+
let buf = "";
|
|
192
|
+
let vis = 0;
|
|
193
|
+
const tokens = normalized.split(
|
|
194
|
+
/(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)))/g
|
|
195
|
+
);
|
|
196
|
+
for (const token of tokens) {
|
|
197
|
+
if (ANSI_FULL_REGEX.test(token)) {
|
|
198
|
+
buf += token;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const ch of [...token]) {
|
|
202
|
+
if (vis >= width) {
|
|
203
|
+
lines.push(this.applyPadding(buf, vis, width, align, padChar));
|
|
204
|
+
buf = "";
|
|
205
|
+
vis = 0;
|
|
206
|
+
}
|
|
207
|
+
buf += ch;
|
|
208
|
+
vis++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (buf) {
|
|
212
|
+
lines.push(this.applyPadding(buf, vis, width, align, padChar));
|
|
213
|
+
}
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
applyPadding(text, visible, width, align, padChar) {
|
|
217
|
+
const pad = Math.max(0, width - visible);
|
|
218
|
+
switch (align) {
|
|
219
|
+
case "right":
|
|
220
|
+
return padChar.repeat(pad) + text;
|
|
221
|
+
case "center": {
|
|
222
|
+
const left = Math.floor(pad / 2);
|
|
223
|
+
const right = pad - left;
|
|
224
|
+
return padChar.repeat(left) + text + padChar.repeat(right);
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return text + padChar.repeat(pad);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ─── Printing ────────────────────────────────────────────
|
|
231
|
+
print(msg) {
|
|
232
|
+
const lines = wrapLine(
|
|
233
|
+
this.showPrompt ? this.prompt + msg : msg,
|
|
234
|
+
MAX_WIDTH,
|
|
235
|
+
0,
|
|
236
|
+
"word"
|
|
237
|
+
);
|
|
238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
239
|
+
this.writeLine(lines[i]);
|
|
240
|
+
if (i < lines.length - 1) process.stdout.write("\n");
|
|
241
|
+
this.addLine(lines[i]);
|
|
242
|
+
}
|
|
243
|
+
this.showPrompt = false;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
println(msg = "") {
|
|
247
|
+
if (msg) this.print(msg);
|
|
248
|
+
process.stdout.write("\n");
|
|
249
|
+
this.addLine("");
|
|
250
|
+
this.showPrompt = true;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
printRaw(line) {
|
|
254
|
+
process.stdout.write(line);
|
|
255
|
+
this.addLine(line, { isRaw: true });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
printlnRaw(line = "") {
|
|
259
|
+
this.printRaw(line);
|
|
260
|
+
process.stdout.write("\n");
|
|
261
|
+
this.addLine("", { isRaw: true });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
error(msg) {
|
|
265
|
+
const lines = wrapLine(
|
|
266
|
+
this.showPrompt ? this.errorPrompt + msg : msg,
|
|
267
|
+
MAX_WIDTH,
|
|
268
|
+
0,
|
|
269
|
+
"word"
|
|
270
|
+
);
|
|
271
|
+
for (let i = 0; i < lines.length; i++) {
|
|
272
|
+
this.writeLine(lines[i], colors.brightRed);
|
|
273
|
+
if (i < lines.length - 1) process.stdout.write("\n");
|
|
274
|
+
this.addLine(lines[i]);
|
|
275
|
+
}
|
|
276
|
+
process.stdout.write("\n");
|
|
277
|
+
this.showPrompt = true;
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
// ─── History ─────────────────────────────────────────────
|
|
281
|
+
clearHistory() {
|
|
282
|
+
this.previousLines = [];
|
|
283
|
+
}
|
|
284
|
+
getHistory() {
|
|
285
|
+
return [...this.previousLines];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const logger = new Logger();
|
|
289
|
+
class JasmineConsoleReporter {
|
|
290
|
+
print = (message) => process.stdout.write(message);
|
|
291
|
+
showColors = true;
|
|
292
|
+
specCount = 0;
|
|
293
|
+
executableSpecCount = 0;
|
|
294
|
+
failureCount = 0;
|
|
295
|
+
failedSpecs = [];
|
|
296
|
+
pendingSpecs = [];
|
|
297
|
+
alwaysListPendingSpecs = true;
|
|
298
|
+
ansi = {
|
|
299
|
+
green: "\x1B[32m",
|
|
300
|
+
red: "\x1B[31m",
|
|
301
|
+
yellow: "\x1B[33m",
|
|
302
|
+
none: "\x1B[0m"
|
|
303
|
+
};
|
|
304
|
+
failedSuites = [];
|
|
305
|
+
stackFilter = (stack) => stack;
|
|
306
|
+
randomSeedReproductionCmd(seed) {
|
|
307
|
+
return "jasmine --random=true --seed=" + seed;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Configures the reporter.
|
|
311
|
+
*/
|
|
312
|
+
setOptions(options) {
|
|
313
|
+
if (options.print) {
|
|
314
|
+
this.print = options.print;
|
|
315
|
+
}
|
|
316
|
+
this.showColors = options.showColors || false;
|
|
317
|
+
if (options.stackFilter) {
|
|
318
|
+
this.stackFilter = options.stackFilter;
|
|
319
|
+
}
|
|
320
|
+
if (options.randomSeedReproductionCmd) {
|
|
321
|
+
this.randomSeedReproductionCmd = options.randomSeedReproductionCmd;
|
|
322
|
+
}
|
|
323
|
+
if (options.alwaysListPendingSpecs !== void 0) {
|
|
324
|
+
this.alwaysListPendingSpecs = options.alwaysListPendingSpecs;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
jasmineStarted(options) {
|
|
328
|
+
this.specCount = 0;
|
|
329
|
+
this.executableSpecCount = 0;
|
|
330
|
+
this.failureCount = 0;
|
|
331
|
+
if (options?.order?.random) {
|
|
332
|
+
this.print("Randomized with seed " + options.order.seed);
|
|
333
|
+
this.printNewline();
|
|
334
|
+
}
|
|
335
|
+
this.print("Started");
|
|
336
|
+
this.printNewline();
|
|
337
|
+
}
|
|
338
|
+
jasmineDone(result) {
|
|
339
|
+
if (result.failedExpectations) {
|
|
340
|
+
this.failureCount += result.failedExpectations.length;
|
|
341
|
+
}
|
|
342
|
+
this.printNewline();
|
|
343
|
+
this.printNewline();
|
|
344
|
+
if (this.failedSpecs.length > 0) {
|
|
345
|
+
this.print("Failures:");
|
|
346
|
+
}
|
|
347
|
+
for (let i = 0; i < this.failedSpecs.length; i++) {
|
|
348
|
+
this.specFailureDetails(this.failedSpecs[i], i + 1);
|
|
349
|
+
}
|
|
350
|
+
for (let i = 0; i < this.failedSuites.length; i++) {
|
|
351
|
+
this.suiteFailureDetails(this.failedSuites[i]);
|
|
352
|
+
}
|
|
353
|
+
if (result.failedExpectations?.length > 0) {
|
|
354
|
+
this.suiteFailureDetails({
|
|
355
|
+
fullName: "top suite",
|
|
356
|
+
failedExpectations: result.failedExpectations
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (this.alwaysListPendingSpecs || result.overallStatus === "passed") {
|
|
360
|
+
if (this.pendingSpecs.length > 0) {
|
|
361
|
+
this.print("Pending:");
|
|
362
|
+
}
|
|
363
|
+
for (let i = 0; i < this.pendingSpecs.length; i++) {
|
|
364
|
+
this.pendingSpecDetails(this.pendingSpecs[i], i + 1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (this.specCount > 0) {
|
|
368
|
+
this.printNewline();
|
|
369
|
+
if (this.executableSpecCount !== this.specCount) {
|
|
370
|
+
this.print(
|
|
371
|
+
"Ran " + this.executableSpecCount + " of " + this.specCount + this.plural(" spec", this.specCount)
|
|
372
|
+
);
|
|
373
|
+
this.printNewline();
|
|
374
|
+
}
|
|
375
|
+
let specCounts = this.executableSpecCount + " " + this.plural("spec", this.executableSpecCount) + ", " + this.failureCount + " " + this.plural("failure", this.failureCount);
|
|
376
|
+
if (this.pendingSpecs.length) {
|
|
377
|
+
specCounts += ", " + this.pendingSpecs.length + " pending " + this.plural("spec", this.pendingSpecs.length);
|
|
378
|
+
}
|
|
379
|
+
this.print(specCounts);
|
|
380
|
+
} else {
|
|
381
|
+
this.print("No specs found");
|
|
382
|
+
}
|
|
383
|
+
this.printNewline();
|
|
384
|
+
const seconds = result ? result.totalTime / 1e3 : 0;
|
|
385
|
+
this.print("Finished in " + seconds + " " + this.plural("second", seconds));
|
|
386
|
+
this.printNewline();
|
|
387
|
+
if (result && result.overallStatus === "incomplete") {
|
|
388
|
+
this.print("Incomplete: " + result.incompleteReason);
|
|
389
|
+
this.printNewline();
|
|
390
|
+
}
|
|
391
|
+
if (result.order?.random) {
|
|
392
|
+
this.print("Randomized with seed " + result.order.seed);
|
|
393
|
+
this.print(" (" + this.randomSeedReproductionCmd(result.order.seed) + ")");
|
|
394
|
+
this.printNewline();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
specDone(result) {
|
|
398
|
+
this.specCount++;
|
|
399
|
+
if (result.status == "pending") {
|
|
400
|
+
this.pendingSpecs.push(result);
|
|
401
|
+
this.executableSpecCount++;
|
|
402
|
+
this.print(this.colored("yellow", "*"));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (result.status == "passed") {
|
|
406
|
+
this.executableSpecCount++;
|
|
407
|
+
this.print(this.colored("green", "."));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (result.status == "failed") {
|
|
411
|
+
this.failureCount++;
|
|
412
|
+
this.failedSpecs.push(result);
|
|
413
|
+
this.executableSpecCount++;
|
|
414
|
+
this.print(this.colored("red", "F"));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
suiteDone(result) {
|
|
418
|
+
if (result.failedExpectations && result.failedExpectations.length > 0) {
|
|
419
|
+
this.failureCount++;
|
|
420
|
+
this.failedSuites.push(result);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
reporterCapabilities = { parallel: true };
|
|
424
|
+
printNewline() {
|
|
425
|
+
this.print("\n");
|
|
426
|
+
}
|
|
427
|
+
colored(color, str) {
|
|
428
|
+
return this.showColors ? this.ansi[color] + str + this.ansi.none : str;
|
|
429
|
+
}
|
|
430
|
+
plural(str, count) {
|
|
431
|
+
return count == 1 ? str : str + "s";
|
|
432
|
+
}
|
|
433
|
+
repeat(thing, times) {
|
|
434
|
+
return Array.from({ length: times }, () => thing);
|
|
435
|
+
}
|
|
436
|
+
indent(str, spaces) {
|
|
437
|
+
const lines = (str || "").split("\n");
|
|
438
|
+
return lines.map((line) => this.repeat(" ", spaces).join("") + line).join("\n");
|
|
439
|
+
}
|
|
440
|
+
specFailureDetails(result, failedSpecNumber) {
|
|
441
|
+
this.printNewline();
|
|
442
|
+
this.print(failedSpecNumber + ") ");
|
|
443
|
+
this.print(result.fullName);
|
|
444
|
+
this.printFailedExpectations(result);
|
|
445
|
+
if (result.debugLogs?.length) {
|
|
446
|
+
this.printNewline();
|
|
447
|
+
this.print(this.indent("Debug logs:", 2));
|
|
448
|
+
this.printNewline();
|
|
449
|
+
for (const entry of result.debugLogs) {
|
|
450
|
+
this.print(this.indent(`${entry.timestamp}ms: ${entry.message}`, 4));
|
|
451
|
+
this.printNewline();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
suiteFailureDetails(result) {
|
|
456
|
+
this.printNewline();
|
|
457
|
+
this.print("Suite error: " + result.fullName);
|
|
458
|
+
this.printFailedExpectations(result);
|
|
459
|
+
}
|
|
460
|
+
printFailedExpectations(result) {
|
|
461
|
+
for (let i = 0; i < result.failedExpectations.length; i++) {
|
|
462
|
+
const failedExpectation = result.failedExpectations[i];
|
|
463
|
+
this.printNewline();
|
|
464
|
+
this.print(this.indent("Message:", 2));
|
|
465
|
+
this.printNewline();
|
|
466
|
+
this.print(this.colored("red", this.indent(failedExpectation.message, 4)));
|
|
467
|
+
this.printNewline();
|
|
468
|
+
this.print(this.indent("Stack:", 2));
|
|
469
|
+
this.printNewline();
|
|
470
|
+
this.print(this.indent(this.stackFilter(failedExpectation.stack), 4));
|
|
471
|
+
}
|
|
472
|
+
if (result.failedExpectations.length === 0 && Array.isArray(result.passedExpectations) && result.passedExpectations.length === 0) {
|
|
473
|
+
this.printNewline();
|
|
474
|
+
this.print(this.indent("Message:", 2));
|
|
475
|
+
this.printNewline();
|
|
476
|
+
this.print(this.colored("red", this.indent("Spec has no expectations", 4)));
|
|
477
|
+
}
|
|
478
|
+
this.printNewline();
|
|
479
|
+
}
|
|
480
|
+
pendingSpecDetails(result, pendingSpecNumber) {
|
|
481
|
+
this.printNewline();
|
|
482
|
+
this.printNewline();
|
|
483
|
+
this.print(pendingSpecNumber + ") ");
|
|
484
|
+
this.print(result.fullName);
|
|
485
|
+
this.printNewline();
|
|
486
|
+
let pendingReason = "No reason given";
|
|
487
|
+
if (result.pendingReason && result.pendingReason !== "") {
|
|
488
|
+
pendingReason = result.pendingReason;
|
|
489
|
+
}
|
|
490
|
+
this.print(this.indent(this.colored("yellow", pendingReason), 2));
|
|
491
|
+
this.printNewline();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
class AwaitableJasmineConsoleReporter extends JasmineConsoleReporter {
|
|
495
|
+
resolveComplete;
|
|
496
|
+
complete;
|
|
497
|
+
constructor() {
|
|
498
|
+
super();
|
|
499
|
+
this.complete = new Promise((resolve) => {
|
|
500
|
+
this.resolveComplete = resolve;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
jasmineDone(result) {
|
|
504
|
+
super.jasmineDone(result);
|
|
505
|
+
this.resolveComplete?.(result);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
class JSONCleaner {
|
|
509
|
+
options;
|
|
510
|
+
constructor(options = {}) {
|
|
511
|
+
this.options = {
|
|
512
|
+
removeComments: options.removeComments !== false,
|
|
513
|
+
removeTrailingCommas: options.removeTrailingCommas !== false,
|
|
514
|
+
normalizeWhitespace: options.normalizeWhitespace === true,
|
|
515
|
+
allowSingleQuotes: options.allowSingleQuotes === true,
|
|
516
|
+
preserveNewlines: options.preserveNewlines !== false,
|
|
517
|
+
strict: options.strict === true
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Clean JSON string by removing comments and fixing common issues
|
|
522
|
+
*/
|
|
523
|
+
clean(jsonString) {
|
|
524
|
+
if (typeof jsonString !== "string") {
|
|
525
|
+
throw new Error("Input must be a string");
|
|
526
|
+
}
|
|
527
|
+
let result = jsonString;
|
|
528
|
+
if (this.options.removeComments) {
|
|
529
|
+
result = this.stripComments(result);
|
|
530
|
+
}
|
|
531
|
+
if (this.options.removeTrailingCommas) {
|
|
532
|
+
result = this.removeTrailingCommas(result);
|
|
533
|
+
}
|
|
534
|
+
if (this.options.allowSingleQuotes) {
|
|
535
|
+
result = this.normalizeSingleQuotes(result);
|
|
536
|
+
}
|
|
537
|
+
if (this.options.normalizeWhitespace) {
|
|
538
|
+
result = this.normalizeWhitespace(result);
|
|
539
|
+
}
|
|
540
|
+
if (this.options.strict) {
|
|
541
|
+
try {
|
|
542
|
+
JSON.parse(result);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
545
|
+
throw new Error(`Invalid JSON after cleaning: ${message}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Strip single-line and multi-line comments
|
|
552
|
+
* More robust than simple regex - handles edge cases
|
|
553
|
+
*/
|
|
554
|
+
stripComments(str) {
|
|
555
|
+
let result = "";
|
|
556
|
+
let i = 0;
|
|
557
|
+
const state = {
|
|
558
|
+
inString: false,
|
|
559
|
+
stringChar: null,
|
|
560
|
+
escapeNext: false
|
|
561
|
+
};
|
|
562
|
+
while (i < str.length) {
|
|
563
|
+
const char = str[i];
|
|
564
|
+
const nextChar = str[i + 1];
|
|
565
|
+
if (state.escapeNext) {
|
|
566
|
+
result += char;
|
|
567
|
+
state.escapeNext = false;
|
|
568
|
+
i++;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (char === "\\" && state.inString) {
|
|
572
|
+
result += char;
|
|
573
|
+
state.escapeNext = true;
|
|
574
|
+
i++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if ((char === '"' || char === "'") && !state.escapeNext) {
|
|
578
|
+
if (!state.inString) {
|
|
579
|
+
state.inString = true;
|
|
580
|
+
state.stringChar = char;
|
|
581
|
+
result += char;
|
|
582
|
+
} else if (char === state.stringChar) {
|
|
583
|
+
state.inString = false;
|
|
584
|
+
state.stringChar = null;
|
|
585
|
+
result += char;
|
|
586
|
+
} else {
|
|
587
|
+
result += char;
|
|
588
|
+
}
|
|
589
|
+
i++;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (!state.inString) {
|
|
593
|
+
if (char === "/" && nextChar === "/") {
|
|
594
|
+
i += 2;
|
|
595
|
+
while (i < str.length && str[i] !== "\n" && str[i] !== "\r") {
|
|
596
|
+
i++;
|
|
597
|
+
}
|
|
598
|
+
if (i < str.length && this.options.preserveNewlines) {
|
|
599
|
+
result += str[i];
|
|
600
|
+
}
|
|
601
|
+
i++;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (char === "/" && nextChar === "*") {
|
|
605
|
+
i += 2;
|
|
606
|
+
let foundEnd = false;
|
|
607
|
+
let newlineCount = 0;
|
|
608
|
+
while (i < str.length - 1) {
|
|
609
|
+
if (str[i] === "\n") newlineCount++;
|
|
610
|
+
if (str[i] === "*" && str[i + 1] === "/") {
|
|
611
|
+
i += 2;
|
|
612
|
+
foundEnd = true;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
i++;
|
|
616
|
+
}
|
|
617
|
+
if (!foundEnd) {
|
|
618
|
+
throw new Error("Unclosed multi-line comment");
|
|
619
|
+
}
|
|
620
|
+
if (this.options.preserveNewlines && newlineCount > 0) {
|
|
621
|
+
result += "\n".repeat(Math.min(newlineCount, 2));
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
result += char;
|
|
627
|
+
i++;
|
|
628
|
+
}
|
|
629
|
+
if (state.inString) {
|
|
630
|
+
throw new Error("Unclosed string in JSON");
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Remove trailing commas before closing brackets/braces
|
|
636
|
+
*/
|
|
637
|
+
removeTrailingCommas(str) {
|
|
638
|
+
let result = "";
|
|
639
|
+
let i = 0;
|
|
640
|
+
const state = {
|
|
641
|
+
inString: false,
|
|
642
|
+
stringChar: null,
|
|
643
|
+
escapeNext: false
|
|
644
|
+
};
|
|
645
|
+
while (i < str.length) {
|
|
646
|
+
const char = str[i];
|
|
647
|
+
if (state.escapeNext) {
|
|
648
|
+
result += char;
|
|
649
|
+
state.escapeNext = false;
|
|
650
|
+
i++;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (char === "\\" && state.inString) {
|
|
654
|
+
result += char;
|
|
655
|
+
state.escapeNext = true;
|
|
656
|
+
i++;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if ((char === '"' || char === "'") && !state.escapeNext) {
|
|
660
|
+
if (!state.inString) {
|
|
661
|
+
state.inString = true;
|
|
662
|
+
state.stringChar = char;
|
|
663
|
+
} else if (char === state.stringChar) {
|
|
664
|
+
state.inString = false;
|
|
665
|
+
state.stringChar = null;
|
|
666
|
+
}
|
|
667
|
+
result += char;
|
|
668
|
+
i++;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (!state.inString && char === ",") {
|
|
672
|
+
let j = i + 1;
|
|
673
|
+
while (j < str.length && /\s/.test(str[j])) {
|
|
674
|
+
j++;
|
|
675
|
+
}
|
|
676
|
+
if (j < str.length && (str[j] === "]" || str[j] === "}")) {
|
|
677
|
+
i++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
result += char;
|
|
682
|
+
i++;
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Convert single quotes to double quotes (outside of strings)
|
|
688
|
+
*/
|
|
689
|
+
normalizeSingleQuotes(str) {
|
|
690
|
+
let result = "";
|
|
691
|
+
let i = 0;
|
|
692
|
+
const state = {
|
|
693
|
+
inString: false,
|
|
694
|
+
stringChar: null,
|
|
695
|
+
escapeNext: false
|
|
696
|
+
};
|
|
697
|
+
while (i < str.length) {
|
|
698
|
+
const char = str[i];
|
|
699
|
+
if (state.escapeNext) {
|
|
700
|
+
result += char;
|
|
701
|
+
state.escapeNext = false;
|
|
702
|
+
i++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (char === "\\" && state.inString) {
|
|
706
|
+
result += char;
|
|
707
|
+
state.escapeNext = true;
|
|
708
|
+
i++;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if ((char === '"' || char === "'") && !state.escapeNext) {
|
|
712
|
+
if (!state.inString) {
|
|
713
|
+
state.inString = true;
|
|
714
|
+
state.stringChar = char;
|
|
715
|
+
result += '"';
|
|
716
|
+
} else if (char === state.stringChar) {
|
|
717
|
+
state.inString = false;
|
|
718
|
+
state.stringChar = null;
|
|
719
|
+
result += '"';
|
|
720
|
+
} else {
|
|
721
|
+
if (char === '"') {
|
|
722
|
+
result += '\\"';
|
|
723
|
+
} else {
|
|
724
|
+
result += char;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
i++;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
result += char;
|
|
731
|
+
i++;
|
|
732
|
+
}
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Normalize whitespace
|
|
737
|
+
*/
|
|
738
|
+
normalizeWhitespace(str) {
|
|
739
|
+
return str.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Parse JSON string with automatic cleaning
|
|
743
|
+
*/
|
|
744
|
+
parse(jsonString) {
|
|
745
|
+
const cleaned = this.clean(jsonString);
|
|
746
|
+
return JSON.parse(cleaned);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Read and clean JSON file
|
|
750
|
+
*/
|
|
751
|
+
readFile(filePath, encoding = "utf8") {
|
|
752
|
+
const content = fs.readFileSync(filePath, encoding);
|
|
753
|
+
return this.clean(content);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Read, clean, and parse JSON file
|
|
757
|
+
*/
|
|
758
|
+
parseFile(filePath, encoding = "utf8") {
|
|
759
|
+
const cleaned = this.readFile(filePath, encoding);
|
|
760
|
+
return JSON.parse(cleaned);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Clean and write JSON file
|
|
764
|
+
*/
|
|
765
|
+
writeFile(filePath, jsonString, encoding = "utf8") {
|
|
766
|
+
const cleaned = this.clean(jsonString);
|
|
767
|
+
fs.writeFileSync(filePath, cleaned, encoding);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Clean and prettify JSON file
|
|
771
|
+
*/
|
|
772
|
+
prettifyFile(inputPath, outputPath = null, indent = 2) {
|
|
773
|
+
const parsed = this.parseFile(inputPath);
|
|
774
|
+
const prettified = JSON.stringify(parsed, null, indent);
|
|
775
|
+
const targetPath = outputPath || inputPath;
|
|
776
|
+
fs.writeFileSync(targetPath, prettified, "utf8");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
780
|
+
const packageRoot = norm(path.resolve(__dirname, ".."));
|
|
781
|
+
const packageRequire = createRequire(path.join(packageRoot, "package.json"));
|
|
782
|
+
const vscodeLaunchConfigName = "Debug current spec (jasmine)";
|
|
783
|
+
function getRuntimeEnv() {
|
|
784
|
+
const runtimeProcess = globalThis.process;
|
|
785
|
+
return runtimeProcess?.env ?? {};
|
|
786
|
+
}
|
|
787
|
+
function isRunningInVsCode() {
|
|
788
|
+
const env = getRuntimeEnv();
|
|
789
|
+
return env.TERM_PROGRAM === "vscode" || typeof env.VSCODE_PID === "string" || typeof env.VSCODE_CWD === "string" || typeof env.VSCODE_INSPECTOR_OPTIONS === "string";
|
|
790
|
+
}
|
|
791
|
+
function printHelp() {
|
|
792
|
+
logger.println("jasmine: run a single Jasmine spec in Node");
|
|
793
|
+
logger.println("");
|
|
794
|
+
logger.println("Usage:");
|
|
795
|
+
logger.println(" npx jasmine --spec <path-to-spec>");
|
|
796
|
+
logger.println(" npx jasmine init");
|
|
797
|
+
logger.println("");
|
|
798
|
+
logger.println("Commands:");
|
|
799
|
+
logger.println(" init Create/update .vscode/launch.json (VS Code debug; requires VS Code)");
|
|
800
|
+
logger.println("");
|
|
801
|
+
logger.println("Options:");
|
|
802
|
+
logger.println(" --spec <path> Path to a single spec file");
|
|
803
|
+
logger.println(" --random Randomize spec order");
|
|
804
|
+
logger.println(" --seed <number> Seed used for randomization");
|
|
805
|
+
logger.println(" --stop-on-fail Stop on first expectation failure");
|
|
806
|
+
logger.println(" --help Show this help");
|
|
807
|
+
logger.println("");
|
|
808
|
+
logger.println("TypeScript + tsconfig paths (recommended):");
|
|
809
|
+
logger.println(
|
|
810
|
+
" node --loader @epikodelabs/testify/esm-loader.mjs ./node_modules/@epikodelabs/testify/bin/jasmine --spec <file>"
|
|
811
|
+
);
|
|
812
|
+
logger.println("");
|
|
813
|
+
logger.println("VS Code debug config name:");
|
|
814
|
+
logger.println(` ${vscodeLaunchConfigName}`);
|
|
815
|
+
}
|
|
816
|
+
function parseArgs(argv) {
|
|
817
|
+
const args = argv.slice(2);
|
|
818
|
+
const get = (flag) => {
|
|
819
|
+
const index = args.indexOf(flag);
|
|
820
|
+
if (index === -1) return void 0;
|
|
821
|
+
return args[index + 1];
|
|
822
|
+
};
|
|
823
|
+
const help = args.includes("--help") || args.includes("-h");
|
|
824
|
+
const command = args[0];
|
|
825
|
+
const initLaunchConfig = args.includes("--init-launch-config") || command === "init";
|
|
826
|
+
const specRaw = get("--spec");
|
|
827
|
+
if (help) {
|
|
828
|
+
return {
|
|
829
|
+
spec: specRaw ? norm(path.resolve(process.cwd(), specRaw)) : "",
|
|
830
|
+
random: args.includes("--random"),
|
|
831
|
+
stopOnFail: args.includes("--stop-on-fail"),
|
|
832
|
+
seed: get("--seed") ? Number(get("--seed")) : void 0,
|
|
833
|
+
help: true,
|
|
834
|
+
initLaunchConfig
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (command && !command.startsWith("-") && command !== "init") {
|
|
838
|
+
logger.error(`ERROR: Unknown command: ${command}`);
|
|
839
|
+
logger.println("");
|
|
840
|
+
printHelp();
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
if (initLaunchConfig) {
|
|
844
|
+
return {
|
|
845
|
+
spec: specRaw ? norm(path.resolve(process.cwd(), specRaw)) : "",
|
|
846
|
+
random: args.includes("--random"),
|
|
847
|
+
stopOnFail: args.includes("--stop-on-fail"),
|
|
848
|
+
seed: get("--seed") ? Number(get("--seed")) : void 0,
|
|
849
|
+
help: false,
|
|
850
|
+
initLaunchConfig: true
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (!specRaw) {
|
|
854
|
+
logger.error("ERROR: Missing required --spec <path>");
|
|
855
|
+
logger.println("");
|
|
856
|
+
printHelp();
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
const spec = norm(path.resolve(process.cwd(), specRaw));
|
|
860
|
+
if (!fs__default.existsSync(spec)) {
|
|
861
|
+
logger.error(`ERROR: Spec file not found: ${spec}`);
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
const seedRaw = get("--seed");
|
|
865
|
+
return {
|
|
866
|
+
spec,
|
|
867
|
+
random: args.includes("--random"),
|
|
868
|
+
stopOnFail: args.includes("--stop-on-fail"),
|
|
869
|
+
seed: seedRaw ? Number(seedRaw) : void 0,
|
|
870
|
+
help: false,
|
|
871
|
+
initLaunchConfig: false
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function normalizeCliArgs(args) {
|
|
875
|
+
const normalized = [...args];
|
|
876
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
877
|
+
if (normalized[i] === "--spec" && typeof normalized[i + 1] === "string") {
|
|
878
|
+
normalized[i + 1] = norm(normalized[i + 1]);
|
|
879
|
+
i += 1;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return normalized;
|
|
883
|
+
}
|
|
884
|
+
function isTypeScriptLike(filePath) {
|
|
885
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
886
|
+
return ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts";
|
|
887
|
+
}
|
|
888
|
+
function hasEsmLoader() {
|
|
889
|
+
const fromNodeOptions = (getRuntimeEnv().NODE_OPTIONS ?? "").split(/\s+/g).filter(Boolean);
|
|
890
|
+
const argv = [...process.execArgv, ...fromNodeOptions];
|
|
891
|
+
return argv.includes("--loader") || argv.some((a) => a.startsWith("--loader="));
|
|
892
|
+
}
|
|
893
|
+
function findNearestTsconfig(startDir) {
|
|
894
|
+
let current = norm(path.resolve(startDir));
|
|
895
|
+
while (true) {
|
|
896
|
+
const candidate = norm(path.join(current, "tsconfig.json"));
|
|
897
|
+
if (fs__default.existsSync(candidate)) return candidate;
|
|
898
|
+
const parent = path.dirname(current);
|
|
899
|
+
if (parent === current) return null;
|
|
900
|
+
current = parent;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function getDefaultVsCodeLaunchConfiguration() {
|
|
904
|
+
return {
|
|
905
|
+
type: "node",
|
|
906
|
+
request: "launch",
|
|
907
|
+
name: vscodeLaunchConfigName,
|
|
908
|
+
runtimeExecutable: "node",
|
|
909
|
+
runtimeArgs: ["--loader", "@epikodelabs/testify/esm-loader.mjs", "--enable-source-maps"],
|
|
910
|
+
program: "${workspaceFolder}/node_modules/@epikodelabs/testify/bin/jasmine",
|
|
911
|
+
args: ["--spec", "${file}"],
|
|
912
|
+
env: {
|
|
913
|
+
TS_NODE_PROJECT: "${workspaceFolder}/tsconfig.json"
|
|
914
|
+
},
|
|
915
|
+
cwd: "${workspaceFolder}",
|
|
916
|
+
console: "integratedTerminal",
|
|
917
|
+
skipFiles: ["<node_internals>/**"]
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function initVsCodeLaunchConfig() {
|
|
921
|
+
const vscodeDir = norm(path.resolve(process.cwd(), ".vscode"));
|
|
922
|
+
const launchJsonPath = norm(path.join(vscodeDir, "launch.json"));
|
|
923
|
+
const config = getDefaultVsCodeLaunchConfiguration();
|
|
924
|
+
fs__default.mkdirSync(vscodeDir, { recursive: true });
|
|
925
|
+
if (!fs__default.existsSync(launchJsonPath)) {
|
|
926
|
+
const launchJson = { version: "0.2.0", configurations: [config] };
|
|
927
|
+
fs__default.writeFileSync(launchJsonPath, `${JSON.stringify(launchJson, null, 2)}
|
|
928
|
+
`);
|
|
929
|
+
logger.println(`Created VS Code launch config at ${launchJsonPath}`);
|
|
930
|
+
logger.println(`Added configuration: ${vscodeLaunchConfigName}`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const raw = fs__default.readFileSync(launchJsonPath, "utf-8");
|
|
934
|
+
let parsed;
|
|
935
|
+
try {
|
|
936
|
+
parsed = JSON.parse(raw);
|
|
937
|
+
} catch {
|
|
938
|
+
try {
|
|
939
|
+
parsed = new JSONCleaner().parse(raw);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
logger.error(`ERROR: Failed to parse existing VS Code launch config: ${launchJsonPath}`);
|
|
942
|
+
logger.error(String(error));
|
|
943
|
+
logger.println("");
|
|
944
|
+
logger.println("Add this configuration manually:");
|
|
945
|
+
logger.println(`${JSON.stringify(getDefaultVsCodeLaunchConfiguration(), null, 2)}`);
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (!parsed || typeof parsed !== "object") parsed = {};
|
|
950
|
+
if (!Array.isArray(parsed.configurations)) parsed.configurations = [];
|
|
951
|
+
const programSuffix = "/bin/jasmine";
|
|
952
|
+
const alreadyHasConfig = parsed.configurations.some((c) => {
|
|
953
|
+
if (!c || typeof c !== "object") return false;
|
|
954
|
+
if (c.name === vscodeLaunchConfigName) return true;
|
|
955
|
+
const program = typeof c.program === "string" ? c.program.replace(/\\/g, "/") : "";
|
|
956
|
+
const args = Array.isArray(c.args) ? c.args : [];
|
|
957
|
+
return program.endsWith(programSuffix) && args.includes("--spec");
|
|
958
|
+
});
|
|
959
|
+
if (alreadyHasConfig) {
|
|
960
|
+
logger.println(`VS Code launch config already contains: ${vscodeLaunchConfigName}`);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
parsed.version ??= "0.2.0";
|
|
964
|
+
parsed.configurations.unshift(config);
|
|
965
|
+
fs__default.writeFileSync(launchJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
966
|
+
`);
|
|
967
|
+
logger.println(`Updated VS Code launch config at ${launchJsonPath}`);
|
|
968
|
+
logger.println(`Added configuration: ${vscodeLaunchConfigName}`);
|
|
969
|
+
}
|
|
970
|
+
async function respawnWithLoader(args) {
|
|
971
|
+
const { spawn } = await __vitePreload(async () => {
|
|
972
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
973
|
+
return { spawn: spawn2 };
|
|
974
|
+
}, true ? [] : void 0);
|
|
975
|
+
const tsconfig = findNearestTsconfig(path.dirname(args.spec));
|
|
976
|
+
const env = { ...getRuntimeEnv() };
|
|
977
|
+
if (tsconfig) env.TS_NODE_PROJECT = tsconfig;
|
|
978
|
+
env.TS_NODE_TRANSPILE_ONLY ??= "true";
|
|
979
|
+
const loaderPath = norm(path.join(packageRoot, "esm-loader.mjs"));
|
|
980
|
+
const loaderSpecifier = fs__default.existsSync(loaderPath) ? pathToFileURL(loaderPath).href : "@epikodelabs/testify/esm-loader.mjs";
|
|
981
|
+
const child = spawn(
|
|
982
|
+
process.execPath,
|
|
983
|
+
[
|
|
984
|
+
"--loader",
|
|
985
|
+
loaderSpecifier,
|
|
986
|
+
"--enable-source-maps",
|
|
987
|
+
process.argv[1],
|
|
988
|
+
...normalizeCliArgs(process.argv.slice(2))
|
|
989
|
+
],
|
|
990
|
+
{ stdio: "inherit", env, cwd: process.cwd() }
|
|
991
|
+
);
|
|
992
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
993
|
+
while (true) {
|
|
994
|
+
await new Promise((r) => setTimeout(r, 1e6));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function loadJasmine() {
|
|
998
|
+
const jasmineCorePath = norm(packageRequire.resolve("jasmine-core/lib/jasmine-core/jasmine.js"));
|
|
999
|
+
const jasmineCore = await import(pathToFileURL(jasmineCorePath).href);
|
|
1000
|
+
const jasmineRequire = jasmineCore.default;
|
|
1001
|
+
const jasmineInstance = jasmineRequire.core(jasmineRequire);
|
|
1002
|
+
const jasmineEnv = jasmineInstance.getEnv();
|
|
1003
|
+
Object.assign(globalThis, jasmineRequire.interface(jasmineInstance, jasmineEnv));
|
|
1004
|
+
globalThis.jasmine = {
|
|
1005
|
+
...globalThis.jasmine ?? {},
|
|
1006
|
+
...jasmineInstance
|
|
1007
|
+
};
|
|
1008
|
+
return { jasmineEnv, jasmineInstance };
|
|
1009
|
+
}
|
|
1010
|
+
async function main() {
|
|
1011
|
+
const args = parseArgs(process.argv);
|
|
1012
|
+
if (args.help) {
|
|
1013
|
+
printHelp();
|
|
1014
|
+
process.exit(0);
|
|
1015
|
+
}
|
|
1016
|
+
if (args.initLaunchConfig) {
|
|
1017
|
+
if (!isRunningInVsCode()) {
|
|
1018
|
+
logger.error("ERROR: `npx jasmine init` is only supported when run from VS Code.");
|
|
1019
|
+
logger.println("");
|
|
1020
|
+
logger.println("Open VS Code, then run this from the integrated terminal (Terminal -> New Terminal).");
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
initVsCodeLaunchConfig();
|
|
1024
|
+
process.exit(0);
|
|
1025
|
+
}
|
|
1026
|
+
if (isTypeScriptLike(args.spec) && !hasEsmLoader() && !process.execArgv.join(" ").includes("--inspect")) {
|
|
1027
|
+
await respawnWithLoader(args);
|
|
1028
|
+
}
|
|
1029
|
+
const { jasmineEnv } = await loadJasmine();
|
|
1030
|
+
process.on("unhandledRejection", (error) => {
|
|
1031
|
+
logger.error(`ERROR: Unhandled rejection: ${error}`);
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
});
|
|
1034
|
+
process.on("uncaughtException", (error) => {
|
|
1035
|
+
logger.error(`ERROR: Uncaught exception: ${error}`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
});
|
|
1038
|
+
jasmineEnv.configure({
|
|
1039
|
+
random: args.random,
|
|
1040
|
+
stopSpecOnExpectationFailure: args.stopOnFail,
|
|
1041
|
+
seed: args.seed
|
|
1042
|
+
});
|
|
1043
|
+
const reporter = new AwaitableJasmineConsoleReporter();
|
|
1044
|
+
jasmineEnv.addReporter(reporter);
|
|
1045
|
+
await import(pathToFileURL(args.spec).href);
|
|
1046
|
+
await jasmineEnv.execute();
|
|
1047
|
+
const exitCode = (await reporter.complete)?.overallStatus === "passed" ? 0 : 1;
|
|
1048
|
+
process.exit(exitCode);
|
|
1049
|
+
}
|
|
1050
|
+
main().catch((error) => {
|
|
1051
|
+
logger.error(`ERROR: Failed to run jasmine: ${error.stack ?? error}`);
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
});
|