@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/bin/testify ADDED
@@ -0,0 +1,4640 @@
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 { fileURLToPath, parse, pathToFileURL } from "url";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import path__default, { extname } from "path";
12
+ import util from "util";
13
+ import { glob } from "glob";
14
+ import { EventEmitter } from "events";
15
+ import * as fs$1 from "fs/promises";
16
+ import http, { createServer } from "http";
17
+ import libCoverage from "istanbul-lib-coverage";
18
+ import libReport from "istanbul-lib-report";
19
+ import libSourceMaps from "istanbul-lib-source-maps";
20
+ import libIstanbulApi from "istanbul-api";
21
+ import { minimatch } from "minimatch";
22
+ import { createInstrumenter } from "istanbul-lib-instrument";
23
+ import { WebSocketServer, WebSocket } from "ws";
24
+ import { watch } from "chokidar";
25
+ import picomatch from "picomatch";
26
+ const norm = (p) => p.replace(/\\/g, "/");
27
+ const capitalize = (p) => {
28
+ if (!p) return "";
29
+ return p.charAt(0).toUpperCase() + p.slice(1);
30
+ };
31
+ class JSONCleaner {
32
+ options;
33
+ constructor(options = {}) {
34
+ this.options = {
35
+ removeComments: options.removeComments !== false,
36
+ removeTrailingCommas: options.removeTrailingCommas !== false,
37
+ normalizeWhitespace: options.normalizeWhitespace === true,
38
+ allowSingleQuotes: options.allowSingleQuotes === true,
39
+ preserveNewlines: options.preserveNewlines !== false,
40
+ strict: options.strict === true
41
+ };
42
+ }
43
+ /**
44
+ * Clean JSON string by removing comments and fixing common issues
45
+ */
46
+ clean(jsonString) {
47
+ if (typeof jsonString !== "string") {
48
+ throw new Error("Input must be a string");
49
+ }
50
+ let result = jsonString;
51
+ if (this.options.removeComments) {
52
+ result = this.stripComments(result);
53
+ }
54
+ if (this.options.removeTrailingCommas) {
55
+ result = this.removeTrailingCommas(result);
56
+ }
57
+ if (this.options.allowSingleQuotes) {
58
+ result = this.normalizeSingleQuotes(result);
59
+ }
60
+ if (this.options.normalizeWhitespace) {
61
+ result = this.normalizeWhitespace(result);
62
+ }
63
+ if (this.options.strict) {
64
+ try {
65
+ JSON.parse(result);
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ throw new Error(`Invalid JSON after cleaning: ${message}`);
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+ /**
74
+ * Strip single-line and multi-line comments
75
+ * More robust than simple regex - handles edge cases
76
+ */
77
+ stripComments(str) {
78
+ let result = "";
79
+ let i = 0;
80
+ const state = {
81
+ inString: false,
82
+ stringChar: null,
83
+ escapeNext: false
84
+ };
85
+ while (i < str.length) {
86
+ const char = str[i];
87
+ const nextChar = str[i + 1];
88
+ if (state.escapeNext) {
89
+ result += char;
90
+ state.escapeNext = false;
91
+ i++;
92
+ continue;
93
+ }
94
+ if (char === "\\" && state.inString) {
95
+ result += char;
96
+ state.escapeNext = true;
97
+ i++;
98
+ continue;
99
+ }
100
+ if ((char === '"' || char === "'") && !state.escapeNext) {
101
+ if (!state.inString) {
102
+ state.inString = true;
103
+ state.stringChar = char;
104
+ result += char;
105
+ } else if (char === state.stringChar) {
106
+ state.inString = false;
107
+ state.stringChar = null;
108
+ result += char;
109
+ } else {
110
+ result += char;
111
+ }
112
+ i++;
113
+ continue;
114
+ }
115
+ if (!state.inString) {
116
+ if (char === "/" && nextChar === "/") {
117
+ i += 2;
118
+ while (i < str.length && str[i] !== "\n" && str[i] !== "\r") {
119
+ i++;
120
+ }
121
+ if (i < str.length && this.options.preserveNewlines) {
122
+ result += str[i];
123
+ }
124
+ i++;
125
+ continue;
126
+ }
127
+ if (char === "/" && nextChar === "*") {
128
+ i += 2;
129
+ let foundEnd = false;
130
+ let newlineCount = 0;
131
+ while (i < str.length - 1) {
132
+ if (str[i] === "\n") newlineCount++;
133
+ if (str[i] === "*" && str[i + 1] === "/") {
134
+ i += 2;
135
+ foundEnd = true;
136
+ break;
137
+ }
138
+ i++;
139
+ }
140
+ if (!foundEnd) {
141
+ throw new Error("Unclosed multi-line comment");
142
+ }
143
+ if (this.options.preserveNewlines && newlineCount > 0) {
144
+ result += "\n".repeat(Math.min(newlineCount, 2));
145
+ }
146
+ continue;
147
+ }
148
+ }
149
+ result += char;
150
+ i++;
151
+ }
152
+ if (state.inString) {
153
+ throw new Error("Unclosed string in JSON");
154
+ }
155
+ return result;
156
+ }
157
+ /**
158
+ * Remove trailing commas before closing brackets/braces
159
+ */
160
+ removeTrailingCommas(str) {
161
+ let result = "";
162
+ let i = 0;
163
+ const state = {
164
+ inString: false,
165
+ stringChar: null,
166
+ escapeNext: false
167
+ };
168
+ while (i < str.length) {
169
+ const char = str[i];
170
+ if (state.escapeNext) {
171
+ result += char;
172
+ state.escapeNext = false;
173
+ i++;
174
+ continue;
175
+ }
176
+ if (char === "\\" && state.inString) {
177
+ result += char;
178
+ state.escapeNext = true;
179
+ i++;
180
+ continue;
181
+ }
182
+ if ((char === '"' || char === "'") && !state.escapeNext) {
183
+ if (!state.inString) {
184
+ state.inString = true;
185
+ state.stringChar = char;
186
+ } else if (char === state.stringChar) {
187
+ state.inString = false;
188
+ state.stringChar = null;
189
+ }
190
+ result += char;
191
+ i++;
192
+ continue;
193
+ }
194
+ if (!state.inString && char === ",") {
195
+ let j = i + 1;
196
+ while (j < str.length && /\s/.test(str[j])) {
197
+ j++;
198
+ }
199
+ if (j < str.length && (str[j] === "]" || str[j] === "}")) {
200
+ i++;
201
+ continue;
202
+ }
203
+ }
204
+ result += char;
205
+ i++;
206
+ }
207
+ return result;
208
+ }
209
+ /**
210
+ * Convert single quotes to double quotes (outside of strings)
211
+ */
212
+ normalizeSingleQuotes(str) {
213
+ let result = "";
214
+ let i = 0;
215
+ const state = {
216
+ inString: false,
217
+ stringChar: null,
218
+ escapeNext: false
219
+ };
220
+ while (i < str.length) {
221
+ const char = str[i];
222
+ if (state.escapeNext) {
223
+ result += char;
224
+ state.escapeNext = false;
225
+ i++;
226
+ continue;
227
+ }
228
+ if (char === "\\" && state.inString) {
229
+ result += char;
230
+ state.escapeNext = true;
231
+ i++;
232
+ continue;
233
+ }
234
+ if ((char === '"' || char === "'") && !state.escapeNext) {
235
+ if (!state.inString) {
236
+ state.inString = true;
237
+ state.stringChar = char;
238
+ result += '"';
239
+ } else if (char === state.stringChar) {
240
+ state.inString = false;
241
+ state.stringChar = null;
242
+ result += '"';
243
+ } else {
244
+ if (char === '"') {
245
+ result += '\\"';
246
+ } else {
247
+ result += char;
248
+ }
249
+ }
250
+ i++;
251
+ continue;
252
+ }
253
+ result += char;
254
+ i++;
255
+ }
256
+ return result;
257
+ }
258
+ /**
259
+ * Normalize whitespace
260
+ */
261
+ normalizeWhitespace(str) {
262
+ return str.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
263
+ }
264
+ /**
265
+ * Parse JSON string with automatic cleaning
266
+ */
267
+ parse(jsonString) {
268
+ const cleaned = this.clean(jsonString);
269
+ return JSON.parse(cleaned);
270
+ }
271
+ /**
272
+ * Read and clean JSON file
273
+ */
274
+ readFile(filePath, encoding = "utf8") {
275
+ const content = fs.readFileSync(filePath, encoding);
276
+ return this.clean(content);
277
+ }
278
+ /**
279
+ * Read, clean, and parse JSON file
280
+ */
281
+ parseFile(filePath, encoding = "utf8") {
282
+ const cleaned = this.readFile(filePath, encoding);
283
+ return JSON.parse(cleaned);
284
+ }
285
+ /**
286
+ * Clean and write JSON file
287
+ */
288
+ writeFile(filePath, jsonString, encoding = "utf8") {
289
+ const cleaned = this.clean(jsonString);
290
+ fs.writeFileSync(filePath, cleaned, encoding);
291
+ }
292
+ /**
293
+ * Clean and prettify JSON file
294
+ */
295
+ prettifyFile(inputPath, outputPath = null, indent = 2) {
296
+ const parsed = this.parseFile(inputPath);
297
+ const prettified = JSON.stringify(parsed, null, indent);
298
+ const targetPath = outputPath || inputPath;
299
+ fs.writeFileSync(targetPath, prettified, "utf8");
300
+ }
301
+ }
302
+ const MAX_WIDTH = 63;
303
+ class ConsoleReporter {
304
+ print;
305
+ showColors;
306
+ specCount;
307
+ executableSpecCount;
308
+ failureCount;
309
+ failedSpecs;
310
+ pendingSpecs;
311
+ ansi;
312
+ startTime;
313
+ config = null;
314
+ envInfo;
315
+ rootSuite;
316
+ currentSuite;
317
+ suiteStack;
318
+ currentSpec;
319
+ suiteById = /* @__PURE__ */ new Map();
320
+ specById = /* @__PURE__ */ new Map();
321
+ lineWidth = MAX_WIDTH;
322
+ interruptHandlersRegistered = false;
323
+ interrupted = false;
324
+ orderedSuites = null;
325
+ orderedSpecs = null;
326
+ constructor() {
327
+ this.print = (...args) => logger.printRaw(util.format(...args));
328
+ this.showColors = this.detectColorSupport();
329
+ this.config = null;
330
+ this.specCount = 0;
331
+ this.executableSpecCount = 0;
332
+ this.failureCount = 0;
333
+ this.failedSpecs = [];
334
+ this.pendingSpecs = [];
335
+ this.startTime = 0;
336
+ this.envInfo = null;
337
+ this.rootSuite = this.createRootSuite();
338
+ this.currentSuite = null;
339
+ this.suiteStack = [this.rootSuite];
340
+ this.currentSpec = null;
341
+ this.ansi = {
342
+ green: "\x1B[32m",
343
+ brightGreen: "\x1B[92m",
344
+ red: "\x1B[31m",
345
+ brightRed: "\x1B[91m",
346
+ yellow: "\x1B[33m",
347
+ brightYellow: "\x1B[93m",
348
+ blue: "\x1B[34m",
349
+ brightBlue: "\x1B[94m",
350
+ cyan: "\x1B[36m",
351
+ brightCyan: "\x1B[96m",
352
+ magenta: "\x1B[35m",
353
+ gray: "\x1B[90m",
354
+ white: "\x1B[97m",
355
+ bold: "\x1B[1m",
356
+ dim: "\x1B[2m",
357
+ none: "\x1B[0m"
358
+ };
359
+ }
360
+ getFailureCount() {
361
+ return this.failureCount;
362
+ }
363
+ // Detect if terminal supports colors
364
+ detectColorSupport() {
365
+ if (process.env.NO_COLOR) return false;
366
+ if (process.env.FORCE_COLOR) return true;
367
+ return process.stdout.isTTY ?? false;
368
+ }
369
+ createRootSuite() {
370
+ return {
371
+ id: "suite0",
372
+ description: "Jasmine__TopLevel__Suite",
373
+ fullName: "",
374
+ specs: [],
375
+ children: [],
376
+ status: "skipped"
377
+ };
378
+ }
379
+ buildSuiteTree(config) {
380
+ this.rootSuite = this.createRootSuite();
381
+ this.suiteById.clear();
382
+ this.specById.clear();
383
+ this.suiteStack = [this.rootSuite];
384
+ this.currentSuite = null;
385
+ this.currentSpec = null;
386
+ this.suiteById.set(this.rootSuite.id, this.rootSuite);
387
+ const orderedSuites = this.orderedSuites;
388
+ if (orderedSuites) {
389
+ orderedSuites.forEach((suiteConfig) => {
390
+ const suite = {
391
+ id: suiteConfig.id,
392
+ description: this.normalizeDescription(suiteConfig.description ?? suiteConfig.id),
393
+ fullName: suiteConfig.fullName ?? suiteConfig.id,
394
+ specs: [],
395
+ children: [],
396
+ parent: void 0,
397
+ status: "skipped"
398
+ // default until executed
399
+ };
400
+ this.suiteById.set(suite.id, suite);
401
+ });
402
+ }
403
+ const orderedSpecs = this.orderedSpecs;
404
+ if (orderedSpecs) {
405
+ orderedSpecs.forEach((specConfig) => {
406
+ const spec = {
407
+ id: specConfig.id,
408
+ description: specConfig.description ?? specConfig.id,
409
+ fullName: specConfig.fullName ?? specConfig.id,
410
+ status: "skipped"
411
+ };
412
+ this.specById.set(spec.id, spec);
413
+ const parentSuiteId = specConfig.suiteId ?? this.findSuiteIdForSpec(specConfig);
414
+ const parentSuite = this.suiteById.get(parentSuiteId);
415
+ if (parentSuite && parentSuite.id !== this.rootSuite.id) {
416
+ parentSuite.specs.push(spec);
417
+ } else {
418
+ this.rootSuite.specs.push(spec);
419
+ }
420
+ });
421
+ }
422
+ if (orderedSuites) {
423
+ orderedSuites.forEach((suiteConfig) => {
424
+ const suite = this.suiteById.get(suiteConfig.id);
425
+ if (!suite) return;
426
+ const parentSuiteId = suiteConfig.parentSuiteId ?? this.findParentSuiteId(suiteConfig);
427
+ const parentSuite = this.suiteById.get(parentSuiteId) ?? this.rootSuite;
428
+ if (parentSuite.id !== suite.id) {
429
+ suite.parent = parentSuite;
430
+ if (!parentSuite.children.includes(suite)) {
431
+ parentSuite.children.push(suite);
432
+ }
433
+ }
434
+ });
435
+ }
436
+ const totalSuites = this.orderedSuites?.length;
437
+ const totalSpecs = this.orderedSpecs?.length;
438
+ logger.println(`๐Ÿงฉ Suite tree built (${totalSuites} suites, ${totalSpecs} specs).`);
439
+ }
440
+ countSpecs(suite) {
441
+ let total = suite.specs.length;
442
+ for (const child of suite.children) {
443
+ total += this.countSpecs(child);
444
+ }
445
+ return total;
446
+ }
447
+ normalizeDescription(desc) {
448
+ if (typeof desc === "string") return desc;
449
+ if (desc?.en) return desc.en;
450
+ return JSON.stringify(desc);
451
+ }
452
+ findSuiteIdForSpec(specConfig) {
453
+ if (specConfig.suiteId) return specConfig.suiteId;
454
+ if (specConfig.fullName) {
455
+ for (const [id, suite] of this.suiteById) {
456
+ if (id !== this.rootSuite.id && specConfig.fullName.startsWith(suite.fullName)) {
457
+ return id;
458
+ }
459
+ }
460
+ }
461
+ return this.rootSuite.id;
462
+ }
463
+ findParentSuiteId(suiteConfig) {
464
+ if (suiteConfig.parentSuiteId) return suiteConfig.parentSuiteId;
465
+ if (suiteConfig.fullName) {
466
+ const parts = suiteConfig.fullName.split(" ");
467
+ if (parts.length > 1) {
468
+ const parentFullName = parts.slice(0, -1).join(" ");
469
+ for (const [id, suite] of this.suiteById) {
470
+ if (suite.fullName === parentFullName) {
471
+ return id;
472
+ }
473
+ }
474
+ }
475
+ }
476
+ return this.rootSuite.id;
477
+ }
478
+ userAgent(message, suites, specs) {
479
+ this.envInfo = this.gatherEnvironmentInfo();
480
+ this.orderedSuites = suites ?? null;
481
+ this.orderedSpecs = specs ?? null;
482
+ if (message) {
483
+ const userAgent = { ...message };
484
+ delete userAgent?.timestamp;
485
+ delete userAgent?.type;
486
+ this.envInfo = {
487
+ ...this.envInfo,
488
+ userAgent
489
+ };
490
+ }
491
+ }
492
+ jasmineStarted(config) {
493
+ this.startTime = Date.now();
494
+ this.specCount = 0;
495
+ this.executableSpecCount = 0;
496
+ this.failureCount = 0;
497
+ this.config = config;
498
+ this.failedSpecs = [];
499
+ this.pendingSpecs = [];
500
+ this.rootSuite = this.createRootSuite();
501
+ this.suiteStack = [this.rootSuite];
502
+ this.currentSuite = null;
503
+ this.currentSpec = null;
504
+ this.interrupted = false;
505
+ this.buildSuiteTree(config);
506
+ this.setupInterruptHandler();
507
+ this.print("\n");
508
+ this.printBox("Test Runner Started", "cyan");
509
+ this.printEnvironmentInfo();
510
+ this.printTestConfiguration(config);
511
+ }
512
+ suiteStarted(config) {
513
+ if (this.interrupted) return;
514
+ let suite = this.suiteById.get(config.id);
515
+ const parentSuite = this.suiteStack[this.suiteStack.length - 1];
516
+ if (!suite) {
517
+ suite = {
518
+ id: config.id,
519
+ description: config.description,
520
+ fullName: config.fullName,
521
+ specs: [],
522
+ children: [],
523
+ parent: parentSuite,
524
+ status: "running"
525
+ };
526
+ this.suiteById.set(suite.id, suite);
527
+ } else {
528
+ suite.status = "running";
529
+ suite.parent = parentSuite;
530
+ }
531
+ if (parentSuite && !parentSuite.children.includes(suite)) {
532
+ parentSuite.children.push(suite);
533
+ }
534
+ this.suiteStack.push(suite);
535
+ this.currentSuite = suite;
536
+ if (config.description) {
537
+ this.clearCurrentLine();
538
+ this.printSuiteLine(suite, false);
539
+ }
540
+ }
541
+ specStarted(config) {
542
+ if (this.interrupted) return;
543
+ const spec = this.specById.get(config.id) ?? {
544
+ id: config.id,
545
+ description: config.description,
546
+ fullName: config.fullName,
547
+ status: "running"
548
+ };
549
+ spec.status = "running";
550
+ this.specById.set(spec.id, spec);
551
+ this.currentSpec = spec;
552
+ if (this.currentSuite) {
553
+ if (!this.currentSuite.specs.includes(spec)) {
554
+ this.currentSuite.specs.push(spec);
555
+ }
556
+ } else {
557
+ this.rootSuite.specs.push(spec);
558
+ }
559
+ this.updateStatusLine();
560
+ }
561
+ specDone(result) {
562
+ if (this.interrupted) return;
563
+ this.specCount++;
564
+ const spec = this.specById.get(result.id);
565
+ if (spec) {
566
+ spec.status = result.status;
567
+ spec.duration = result.duration;
568
+ spec.failedExpectations = result.failedExpectations;
569
+ spec.pendingReason = result.pendingReason;
570
+ }
571
+ switch (result.status) {
572
+ case "passed":
573
+ this.executableSpecCount++;
574
+ break;
575
+ case "failed":
576
+ this.failureCount++;
577
+ this.failedSpecs.push(result);
578
+ this.executableSpecCount++;
579
+ break;
580
+ case "pending":
581
+ this.pendingSpecs.push(result);
582
+ this.executableSpecCount++;
583
+ break;
584
+ }
585
+ this.currentSpec = null;
586
+ if (this.currentSuite) {
587
+ this.clearCurrentLine();
588
+ this.printSuiteLine(this.currentSuite, false);
589
+ this.updateStatusLine();
590
+ }
591
+ }
592
+ suiteDone(result) {
593
+ if (this.interrupted) return;
594
+ const suite = this.suiteStack[this.suiteStack.length - 1];
595
+ if (!suite) return;
596
+ suite.status = this.determineSuiteStatusFromInternal(suite);
597
+ this.clearCurrentLine();
598
+ this.printSuiteLine(suite, true);
599
+ this.print("\n");
600
+ this.suiteStack.pop();
601
+ this.currentSuite = this.suiteStack[this.suiteStack.length - 1] ?? null;
602
+ }
603
+ jasmineDone(result) {
604
+ const totalTimeMs = typeof result?.totalTime === "number" ? result.totalTime : Date.now() - this.startTime;
605
+ const totalTime = totalTimeMs / 1e3;
606
+ this.clearCurrentLine();
607
+ if (this.failedSpecs.length > 0) {
608
+ this.printFailures();
609
+ }
610
+ if (this.pendingSpecs.length > 0) {
611
+ this.printPendingSpecs();
612
+ }
613
+ this.printSummary(totalTime);
614
+ this.print("\n");
615
+ this.printFinalStatus(result?.overallStatus);
616
+ this.print("\n\n");
617
+ }
618
+ /** Mark all specs and suites that were never executed as skipped */
619
+ markUnexecutedAsSkipped() {
620
+ if (this.currentSpec) {
621
+ if (!this.currentSpec.status || this.currentSpec.status === "running") {
622
+ this.currentSpec.status = "incomplete";
623
+ }
624
+ }
625
+ if (this.currentSuite && this.currentSuite.id !== this.rootSuite.id) {
626
+ if (!this.currentSuite.status || this.currentSuite.status === "running") {
627
+ this.currentSuite.status = "incomplete";
628
+ }
629
+ }
630
+ for (const [id, spec] of this.specById) {
631
+ if (this.currentSpec && id === this.currentSpec.id) continue;
632
+ if (!spec.status || spec.status === "running") {
633
+ spec.status = "skipped";
634
+ }
635
+ }
636
+ for (const [id, suite] of this.suiteById) {
637
+ if (id === this.rootSuite.id) continue;
638
+ if (this.currentSuite && id === this.currentSuite.id) continue;
639
+ if (!suite.status || suite.status === "running") {
640
+ suite.status = "skipped";
641
+ }
642
+ }
643
+ }
644
+ testsAborted(message) {
645
+ this.print("\r\x1B[1A");
646
+ this.clearCurrentLine();
647
+ this.clearCurrentLine();
648
+ this.print("\n");
649
+ this.markUnexecutedAsSkipped();
650
+ const totalTime = (Date.now() - this.startTime) / 1e3;
651
+ if (this.failedSpecs.length > 0) {
652
+ this.printFailures();
653
+ }
654
+ this.printTestTree();
655
+ this.print("\n");
656
+ this.printSummary(totalTime);
657
+ this.print("\n");
658
+ this.printBox("โœ• TESTS INTERRUPTED", "yellow");
659
+ this.print("\n");
660
+ process.exit(1);
661
+ }
662
+ /** Determine suite status based on its specs and children */
663
+ determineSuiteStatusFromInternal(suite) {
664
+ if (suite.specs.length > 0) {
665
+ const hasFailed = suite.specs.some((s) => s.status === "failed");
666
+ if (hasFailed) return "failed";
667
+ const hasPending = suite.specs.some((s) => s.status === "pending");
668
+ if (hasPending) return "pending";
669
+ const hasRunning = suite.specs.some((s) => s.status === "running");
670
+ if (hasRunning) return "incomplete";
671
+ const hasSkipped = suite.specs.every((s) => s.status === "skipped");
672
+ if (hasSkipped) return "skipped";
673
+ return "passed";
674
+ }
675
+ if (suite.children.length > 0) {
676
+ let childStatuses = suite.children.map((child) => this.determineSuiteStatusFromInternal(child));
677
+ if (childStatuses.includes("failed")) return "failed";
678
+ if (childStatuses.includes("pending")) return "pending";
679
+ if (childStatuses.includes("incomplete")) return "incomplete";
680
+ if (childStatuses.every((s) => s === "skipped")) return "skipped";
681
+ return "passed";
682
+ }
683
+ return "skipped";
684
+ }
685
+ setupInterruptHandler() {
686
+ if (this.interruptHandlersRegistered) return;
687
+ process.once("SIGINT", this.testsAborted.bind(this));
688
+ process.once("SIGTERM", this.testsAborted.bind(this));
689
+ this.interruptHandlersRegistered = true;
690
+ }
691
+ updateStatusLine() {
692
+ if (!this.currentSuite || !this.currentSpec) return;
693
+ const suiteName = this.currentSuite.description;
694
+ const passed = this.executableSpecCount - this.failureCount - this.pendingSpecs.length;
695
+ const statusText = `
696
+ ${this.colored("dim", "โ†’")} ${suiteName} ${this.colored("gray", `[${passed}/${this.executableSpecCount} passed]`)}`;
697
+ this.clearCurrentLine();
698
+ this.print(statusText);
699
+ this.print("\r\x1B[1A");
700
+ }
701
+ clearCurrentLine() {
702
+ this.print("\x1B[2K\r");
703
+ }
704
+ printSuiteLine(suite, isFinal) {
705
+ const suiteName = suite.description;
706
+ let displayDots = this.getSpecDots(suite);
707
+ const prefix = " ";
708
+ const availableWidth = this.lineWidth - prefix.length;
709
+ let displayName = suiteName;
710
+ const suiteNameLength = displayName.replace(/\.\.\.$/, "").length + (displayName.includes("...") ? 3 : 0);
711
+ const dotsLength = this.countVisualDots(displayDots);
712
+ let padding = " ".repeat(Math.max(0, availableWidth - suiteNameLength - dotsLength));
713
+ this.print(prefix + this.colored("brightBlue", displayName) + padding + displayDots);
714
+ if (!isFinal) {
715
+ this.print("\r");
716
+ }
717
+ }
718
+ getSpecDots(suite) {
719
+ return suite.specs.map((spec) => this.getSpecSymbol(spec)).join("");
720
+ }
721
+ getSpecSymbol(spec) {
722
+ switch (spec.status) {
723
+ case "passed":
724
+ return this.colored("brightGreen", "โ—");
725
+ case "failed":
726
+ return this.colored("brightRed", "โจฏ");
727
+ case "pending":
728
+ return this.colored("brightYellow", "โ—‹");
729
+ default:
730
+ return "";
731
+ }
732
+ }
733
+ compressDots(suite, sideCount) {
734
+ const dots = suite.specs.map((spec) => this.getSpecSymbol(spec));
735
+ if (dots.length <= sideCount * 2) {
736
+ return dots.join("");
737
+ }
738
+ const start = dots.slice(0, sideCount).join("");
739
+ const end = dots.slice(-sideCount).join("");
740
+ const ellipsis = this.colored("gray", "...");
741
+ return start + ellipsis + end;
742
+ }
743
+ countVisualDots(dotsString) {
744
+ return dotsString.replace(/\x1b\[[0-9;]*m/g, "").length;
745
+ }
746
+ separator() {
747
+ return " " + "โ”€".repeat(this.lineWidth - 2);
748
+ }
749
+ printFailures() {
750
+ this.print("\n");
751
+ this.printSectionHeader("FAILURES", "red");
752
+ this.print(this.colored("red", this.separator() + "\n"));
753
+ if (!this.failedSpecs.length) return;
754
+ this.failedSpecs.forEach((spec, idx) => {
755
+ const header = wrapLine(`${idx + 1}) ${spec.fullName}`, this.lineWidth, 1);
756
+ header.forEach((line) => (this.print(this.colored("white", line)), this.print("\n")));
757
+ if (spec.failedExpectations?.length > 0) {
758
+ spec.failedExpectations.forEach((expectation, exIndex) => {
759
+ const messageLines = wrapLine(`โœ• ${logger.reformat(expectation.message, { width: this.lineWidth, align: "left" }).map((l) => l.trim()).join(" ")}`, this.lineWidth, 1);
760
+ messageLines.forEach((line) => (this.print(this.colored("brightRed", line)), this.print("\n")));
761
+ if (expectation.stack) {
762
+ const stackLines = wrapLine(logger.reformat(expectation.stack, { width: 80, align: "left" }).map((l) => l.trim()).join(" "), this.lineWidth, 2);
763
+ stackLines.forEach((line) => (this.print(this.colored("gray", line)), this.print("\n")));
764
+ }
765
+ if (exIndex < spec.failedExpectations.length - 1) this.print("\n");
766
+ });
767
+ }
768
+ this.print("\n");
769
+ });
770
+ }
771
+ printPendingSpecs() {
772
+ this.print("\n");
773
+ this.printSectionHeader("PENDING", "yellow");
774
+ this.print(this.colored("yellow", this.separator() + "\n"));
775
+ this.pendingSpecs.forEach((spec, idx) => {
776
+ const header = wrapLine(`${this.colored("brightYellow", "โ—‹")} ${this.colored("white", spec.fullName)}`, this.lineWidth, 1, "word");
777
+ header.forEach((line) => (this.print(line), this.print("\n")));
778
+ });
779
+ }
780
+ computeOverallStatus(jasmineOverallStatus) {
781
+ if (this.failureCount > 0) return "failed";
782
+ const hasIncompleteSpec = [...this.specById.values()].some(
783
+ (spec) => spec.status === "incomplete" || spec.status === "running"
784
+ );
785
+ const hasIncompleteSuite = [...this.suiteById.values()].some(
786
+ (suite) => suite.status === "incomplete" || suite.status === "running"
787
+ );
788
+ if (hasIncompleteSpec || hasIncompleteSuite || jasmineOverallStatus === "incomplete") {
789
+ return "incomplete";
790
+ }
791
+ return "passed";
792
+ }
793
+ printFinalStatus(jasmineOverallStatus) {
794
+ const overallStatus = this.computeOverallStatus(jasmineOverallStatus);
795
+ if (overallStatus === "passed") {
796
+ const msg = this.pendingSpecs.length === 0 ? "โœ“ ALL TESTS PASSED" : `โœ“ ALL TESTS PASSED (${this.pendingSpecs.length} pending)`;
797
+ this.printBox(msg, "green");
798
+ } else if (overallStatus === "failed") {
799
+ this.printBox(`โœ• ${this.failureCount} TEST${this.failureCount === 1 ? "" : "S"} FAILED`, "red");
800
+ } else if (overallStatus === "incomplete") {
801
+ this.printBox("โš  TESTS INCOMPLETE", "yellow");
802
+ } else {
803
+ this.printBox(`โš  UNKNOWN STATUS: ${overallStatus}`, "red");
804
+ }
805
+ }
806
+ printTestTree() {
807
+ this.print(this.colored("bold", " Demanding Attention\n"));
808
+ this.print(this.colored("gray", this.separator() + "\n"));
809
+ for (const [id, suite] of this.suiteById) {
810
+ this.calculateSuiteStatuses(suite);
811
+ }
812
+ let hasProblems = false;
813
+ for (const [id, suite] of this.suiteById) {
814
+ if (suite.id === this.rootSuite.id) continue;
815
+ if (!suite.parent || suite.parent.id === this.rootSuite.id) {
816
+ if (this.printProblemSuite(suite, 1)) {
817
+ hasProblems = true;
818
+ }
819
+ }
820
+ }
821
+ if (!hasProblems) {
822
+ this.print(" " + this.colored("brightGreen", "โœ“") + " " + this.colored("dim", "All suites completed successfully\n"));
823
+ }
824
+ }
825
+ calculateSuiteStatuses(suite) {
826
+ const childStatuses = suite.children.map((child) => this.calculateSuiteStatuses(child));
827
+ for (const spec of suite.specs) {
828
+ if (!spec.status) {
829
+ spec.status = "skipped";
830
+ } else if (spec.status === "running") {
831
+ spec.status = "incomplete";
832
+ }
833
+ }
834
+ const specs = suite.specs;
835
+ const failedCount = specs.filter((s) => s.status === "failed").length;
836
+ const pendingCount = specs.filter((s) => s.status === "pending").length;
837
+ const incompleteCount = specs.filter((s) => s.status === "incomplete").length;
838
+ const skippedCount = specs.filter((s) => s.status === "skipped").length;
839
+ const hasFailedChildren = childStatuses.includes("failed");
840
+ const hasPendingChildren = childStatuses.includes("pending");
841
+ const hasIncompleteChildren = childStatuses.includes("incomplete");
842
+ if (failedCount > 0 || hasFailedChildren) {
843
+ suite.status = "failed";
844
+ } else if (incompleteCount > 0 || hasIncompleteChildren) {
845
+ suite.status = "incomplete";
846
+ } else if (pendingCount > 0 || hasPendingChildren) {
847
+ suite.status = "pending";
848
+ } else if (skippedCount > 0) {
849
+ suite.status = "skipped";
850
+ } else {
851
+ suite.status = "passed";
852
+ }
853
+ return suite.status;
854
+ }
855
+ /** Print only suites/specs that need attention */
856
+ printProblemSuite(suite, indentLevel = 0) {
857
+ if (suite.id === this.rootSuite.id) return false;
858
+ const indent = " ".repeat(indentLevel);
859
+ let hasProblems = false;
860
+ let childHasProblems = false;
861
+ for (const child of suite.children) {
862
+ if (this.printProblemSuite(child, indentLevel + 1)) {
863
+ childHasProblems = true;
864
+ hasProblems = true;
865
+ }
866
+ }
867
+ const isProblemSuite = ["failed", "pending", "incomplete", "skipped"].includes(suite.status);
868
+ if (isProblemSuite || childHasProblems) {
869
+ hasProblems = true;
870
+ if (isProblemSuite) {
871
+ const { symbol, color } = this.getSuiteSymbol(suite.status);
872
+ const specs = suite.specs;
873
+ const specCount = specs.length;
874
+ const failed = specs.filter((s) => s.status === "failed").length;
875
+ const pending = specs.filter((s) => s.status === "pending").length;
876
+ const incomplete = specs.filter((s) => s.status === "incomplete").length;
877
+ const skipped = specs.filter((s) => s.status === "skipped").length;
878
+ const passed = specs.filter((s) => s.status === "passed").length;
879
+ const statusParts = [];
880
+ switch (suite.status) {
881
+ case "failed":
882
+ if (failed > 0) statusParts.push(`${failed} failed`);
883
+ if (passed > 0) statusParts.push(`${passed} passed`);
884
+ if (pending > 0) statusParts.push(`${pending} pending`);
885
+ if (incomplete > 0) statusParts.push(`${incomplete} incomplete`);
886
+ if (skipped > 0) statusParts.push(`${skipped} skipped`);
887
+ break;
888
+ case "incomplete":
889
+ if (incomplete > 0) statusParts.push(`${incomplete} incomplete`);
890
+ if (passed > 0) statusParts.push(`${passed} passed`);
891
+ if (failed > 0) statusParts.push(`${failed} failed`);
892
+ if (pending > 0) statusParts.push(`${pending} pending`);
893
+ break;
894
+ case "pending":
895
+ if (pending > 0) statusParts.push(`${pending} pending`);
896
+ if (passed > 0) statusParts.push(`${passed} passed`);
897
+ if (failed > 0) statusParts.push(`${failed} failed`);
898
+ break;
899
+ case "skipped":
900
+ statusParts.push(`${specCount} skipped`);
901
+ break;
902
+ case "passed":
903
+ statusParts.push(`${specCount} passed`);
904
+ break;
905
+ }
906
+ if (suite.children.length > 0) {
907
+ statusParts.push(`${suite.children.length} child suite${suite.children.length !== 1 ? "s" : ""}`);
908
+ }
909
+ const details = statusParts.length ? this.colored("gray", ` (${statusParts.join(", ")})`) : "";
910
+ const desc = suite.status === "incomplete" ? this.colored("yellow", suite.description) : suite.description;
911
+ this.print(`${indent}${this.colored(color, symbol)} ${desc}${details}
912
+ `);
913
+ } else if (childHasProblems) {
914
+ const hasNonSkippedChildProblems = suite.children.some(
915
+ (c) => ["failed", "pending", "incomplete"].includes(c.status)
916
+ );
917
+ if (hasNonSkippedChildProblems) {
918
+ this.print(`${indent}${this.colored("brightBlue", "โ†ณ")} ${this.colored("dim", suite.description)}
919
+ `);
920
+ }
921
+ }
922
+ }
923
+ return hasProblems;
924
+ }
925
+ getSuiteSymbol(status) {
926
+ switch (status) {
927
+ case "failed":
928
+ return { symbol: "โœ•", color: "brightRed" };
929
+ case "pending":
930
+ return { symbol: "โ—‹", color: "brightYellow" };
931
+ case "skipped":
932
+ return { symbol: "โคผ", color: "gray" };
933
+ case "incomplete":
934
+ return { symbol: "โ—ท", color: "cyan" };
935
+ case "passed":
936
+ return { symbol: "โœ“", color: "brightGreen" };
937
+ default:
938
+ return { symbol: "?", color: "white" };
939
+ }
940
+ }
941
+ printBox(text, color) {
942
+ const width = text.length + 4;
943
+ const topBottom = "โ•".repeat(width);
944
+ logger.printlnRaw(`${this.colored(color, ` โ•”${topBottom}โ•—`)}`);
945
+ logger.printlnRaw(`${this.colored(color, ` โ•‘ `)}${this.colored(["bold", color], text + ` โ•‘`)}`);
946
+ logger.printlnRaw(`${this.colored(color, ` โ•š${topBottom}โ•`)}`);
947
+ }
948
+ printSectionHeader(text, color) {
949
+ this.print(this.colored("bold", this.colored(color, ` ${text}
950
+ `)));
951
+ }
952
+ printDivider() {
953
+ this.print(this.colored("gray", this.separator() + "\n"));
954
+ }
955
+ printSummary(totalTime) {
956
+ let passed = 0;
957
+ let failed = 0;
958
+ let pending = 0;
959
+ let skipped = 0;
960
+ let incomplete = 0;
961
+ let notRun = 0;
962
+ for (const [id, spec] of this.specById) {
963
+ switch (spec.status) {
964
+ case "passed":
965
+ passed++;
966
+ break;
967
+ case "failed":
968
+ failed++;
969
+ break;
970
+ case "pending":
971
+ pending++;
972
+ break;
973
+ case "skipped":
974
+ skipped++;
975
+ break;
976
+ case "incomplete":
977
+ incomplete++;
978
+ break;
979
+ default:
980
+ notRun++;
981
+ break;
982
+ }
983
+ }
984
+ const totalSpecs = this.specById.size;
985
+ const duration = `${totalTime.toFixed(3)}s`;
986
+ const lineWidth = this.lineWidth;
987
+ const rightInfo = `total: ${totalSpecs} time: ${duration}`;
988
+ const title = " Test Summary";
989
+ const spacing = Math.max(1, lineWidth - title.length - rightInfo.length);
990
+ const headerLine = this.colored("bold", title) + " ".repeat(spacing) + this.colored("gray", rightInfo);
991
+ this.print("\n");
992
+ this.print(headerLine + "\n");
993
+ this.print(this.colored("gray", this.separator() + "\n"));
994
+ const parts = [];
995
+ if (passed > 0)
996
+ parts.push(this.colored("brightGreen", `โœ“ Passed: ${passed}`));
997
+ if (failed > 0)
998
+ parts.push(this.colored("brightRed", `โœ• Failed: ${failed}`));
999
+ if (pending > 0)
1000
+ parts.push(this.colored("brightYellow", `โ—‹ Pending: ${pending}`));
1001
+ if (incomplete > 0)
1002
+ parts.push(this.colored("cyan", `โ—ท Incomplete: ${incomplete}`));
1003
+ if (skipped > 0)
1004
+ parts.push(this.colored("gray", `โคผ Skipped: ${skipped}`));
1005
+ if (notRun > 0)
1006
+ parts.push(this.colored("gray", `โŠ˜ Not Run: ${notRun}`));
1007
+ if (parts.length > 0)
1008
+ this.print(" " + parts.join(this.colored("gray", " | ")) + "\n");
1009
+ else
1010
+ this.print(this.colored("gray", " (no specs executed)\n"));
1011
+ }
1012
+ colored(style, text) {
1013
+ const styles = Array.isArray(style) ? style : [style];
1014
+ const seq = styles.map((s) => this.ansi[s] ?? "").join("");
1015
+ return `${seq}${text}${this.ansi.none}`;
1016
+ }
1017
+ gatherEnvironmentInfo() {
1018
+ const memUsage = process.memoryUsage();
1019
+ const memTotal = Math.round(memUsage.heapTotal / 1024 / 1024);
1020
+ const uptime = Math.round(process.uptime());
1021
+ return {
1022
+ node: process.version,
1023
+ platform: `${process.platform} ${process.arch}`,
1024
+ arch: process.arch,
1025
+ cwd: process.cwd(),
1026
+ memory: `${memTotal} MB`,
1027
+ pid: process.pid,
1028
+ uptime: `${uptime}s`
1029
+ };
1030
+ }
1031
+ printEnvironmentInfo() {
1032
+ if (!this.envInfo) this.envInfo = this.gatherEnvironmentInfo();
1033
+ this.print("\n");
1034
+ this.print(this.colored("bold", " Environment\n"));
1035
+ this.print(this.colored("gray", this.separator() + "\n"));
1036
+ this.print(this.colored("cyan", " Node.js: ") + this.colored("white", `${this.envInfo.node}
1037
+ `));
1038
+ this.print(this.colored("cyan", " Platform: ") + this.colored("white", `${this.envInfo.platform}
1039
+ `));
1040
+ this.print(this.colored("cyan", " Arch: ") + this.colored("white", `${this.envInfo.arch}
1041
+ `));
1042
+ this.print(this.colored("cyan", " PID: ") + this.colored("white", `${this.envInfo.pid}
1043
+ `));
1044
+ this.print(this.colored("cyan", " Uptime: ") + this.colored("white", `${this.envInfo.uptime}
1045
+ `));
1046
+ this.print(this.colored("cyan", " Memory: ") + this.colored("white", `${this.envInfo.memory} heap
1047
+ `));
1048
+ if (this.envInfo.userAgent) {
1049
+ this.printUserAgentInfo(this.envInfo.userAgent);
1050
+ }
1051
+ this.print("\n");
1052
+ const cwdShort = this.truncateString(this.envInfo.cwd, 45, true);
1053
+ this.print(this.colored("cyan", " Directory: ") + this.colored("gray", `${cwdShort}
1054
+ `));
1055
+ }
1056
+ detectBrowser(userAgent) {
1057
+ let name = "Unknown";
1058
+ let version = "";
1059
+ const ua = userAgent.toLowerCase();
1060
+ if (/firefox\/(\d+\.\d+)/.test(ua)) {
1061
+ name = "Firefox";
1062
+ version = ua.match(/firefox\/(\d+\.\d+)/)[1];
1063
+ } else if (/edg\/(\d+\.\d+)/.test(ua)) {
1064
+ name = "Edge";
1065
+ version = ua.match(/edg\/(\d+\.\d+)/)[1];
1066
+ } else if (/chrome\/(\d+\.\d+)/.test(ua)) {
1067
+ name = "Chrome";
1068
+ version = ua.match(/chrome\/(\d+\.\d+)/)[1];
1069
+ } else if (/safari\/(\d+\.\d+)/.test(ua) && /version\/(\d+\.\d+)/.test(ua)) {
1070
+ name = "Safari";
1071
+ version = ua.match(/version\/(\d+\.\d+)/)[1];
1072
+ } else if (/opr\/(\d+\.\d+)/.test(ua)) {
1073
+ name = "Opera";
1074
+ version = ua.match(/opr\/(\d+\.\d+)/)[1];
1075
+ }
1076
+ return { name, version };
1077
+ }
1078
+ printUserAgentInfo(userAgent) {
1079
+ const { name: browserName, version: browserVersion } = this.detectBrowser(userAgent.userAgent);
1080
+ this.print("\n");
1081
+ this.print(this.colored("bold", " Browser/Navigator\n"));
1082
+ this.print(this.colored("gray", this.separator() + "\n"));
1083
+ const shortUA = this.truncateString(userAgent.userAgent, 45);
1084
+ this.print(this.colored("cyan", " User Agent: ") + this.colored("white", `${shortUA}
1085
+ `));
1086
+ this.print(this.colored("cyan", " Browser: ") + this.colored("white", `${browserName} ${browserVersion}
1087
+ `));
1088
+ if (userAgent.platform) {
1089
+ this.print(this.colored("cyan", " Platform: ") + this.colored("white", `${userAgent.platform}
1090
+ `));
1091
+ }
1092
+ if (userAgent.vendor) {
1093
+ this.print(this.colored("cyan", " Vendor: ") + this.colored("white", `${userAgent.vendor}
1094
+ `));
1095
+ }
1096
+ if (userAgent.language) {
1097
+ this.print(this.colored("cyan", " Language: ") + this.colored("white", `${userAgent.language}
1098
+ `));
1099
+ }
1100
+ if (userAgent.languages?.length > 0) {
1101
+ const langs = userAgent.languages.join(", ");
1102
+ const shortLangs = this.truncateString(langs, 40);
1103
+ this.print(this.colored("cyan", " Languages: ") + this.colored("white", `${shortLangs}
1104
+ `));
1105
+ }
1106
+ }
1107
+ truncateString(str, maxLength, fromStart = false) {
1108
+ if (str.length <= maxLength) return str;
1109
+ if (fromStart) {
1110
+ return "..." + str.slice(-(maxLength - 3));
1111
+ }
1112
+ return str.substring(0, maxLength - 3) + "...";
1113
+ }
1114
+ printTestConfiguration(config) {
1115
+ if (!config || Object.keys(config).length === 0) return;
1116
+ const lineWidth = this.lineWidth;
1117
+ const orderPart = config.order?.random !== void 0 ? config.order.random ? "random" : "sequential" : null;
1118
+ const seedPart = config.order?.seed !== void 0 ? `seed: ${config.order.seed}` : null;
1119
+ const rightInfo = [orderPart, seedPart].filter(Boolean).join(" ");
1120
+ const title = " Test Configuration";
1121
+ const spacing = Math.max(1, lineWidth - title.length - rightInfo.length);
1122
+ const headerLine = this.colored("bold", title) + " ".repeat(spacing) + this.colored("gray", rightInfo);
1123
+ this.print("\n");
1124
+ this.print(headerLine + "\n");
1125
+ this.print(this.colored("gray", this.separator() + "\n"));
1126
+ const parts = [];
1127
+ if (config.stopOnSpecFailure !== void 0)
1128
+ parts.push(
1129
+ this.colored("magenta", "Fail Fast:") + " " + this.colored("white", config.stopOnSpecFailure ? "โœ“ enabled" : "โœ— disabled")
1130
+ );
1131
+ if (config.stopSpecOnExpectationFailure !== void 0)
1132
+ parts.push(
1133
+ this.colored("magenta", "Stop Spec:") + " " + this.colored("white", config.stopSpecOnExpectationFailure ? "โœ“ enabled" : "โœ— disabled")
1134
+ );
1135
+ if (config.failSpecWithNoExpectations !== void 0)
1136
+ parts.push(
1137
+ this.colored("magenta", "No Expect:") + " " + this.colored("white", config.failSpecWithNoExpectations ? "โœ“ fail" : "โœ— pass")
1138
+ );
1139
+ if (parts.length > 0) {
1140
+ this.print(" " + parts.join(this.colored("gray", " | ")) + "\n");
1141
+ }
1142
+ }
1143
+ }
1144
+ const ANSI_FULL_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
1145
+ function wrapLine(text, width, indentation = 0, mode = "char") {
1146
+ text = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
1147
+ const indent = " ".repeat(indentation);
1148
+ const indentWidth = indent.length;
1149
+ if (width <= indentWidth) width = indentWidth + 1;
1150
+ const availableWidth = width - indentWidth;
1151
+ return mode === "char" ? wrapByChar(text, availableWidth, indent) : wrapByWord(text, availableWidth, indent);
1152
+ }
1153
+ function wrapByChar(text, available, indent) {
1154
+ const lines = [];
1155
+ let buf = "";
1156
+ let vis = 0;
1157
+ const tokens = text.split(ANSI_FULL_REGEX);
1158
+ for (const token of tokens) {
1159
+ if (ANSI_FULL_REGEX.test(token)) {
1160
+ buf += token;
1161
+ continue;
1162
+ }
1163
+ for (const ch of [...token]) {
1164
+ if (vis >= available) {
1165
+ lines.push(indent + buf);
1166
+ buf = "";
1167
+ vis = 0;
1168
+ }
1169
+ buf += ch;
1170
+ vis++;
1171
+ }
1172
+ }
1173
+ if (buf) lines.push(indent + buf);
1174
+ return lines;
1175
+ }
1176
+ function wrapByWord(text, available, indent) {
1177
+ const lines = [];
1178
+ let buf = "";
1179
+ let vis = 0;
1180
+ let word = "";
1181
+ let wordVis = 0;
1182
+ const flushWord = () => {
1183
+ if (!word) return;
1184
+ if (wordVis > available) {
1185
+ for (const ch of [...word]) {
1186
+ if (vis >= available) {
1187
+ lines.push(indent + buf.trimEnd());
1188
+ buf = "";
1189
+ vis = 0;
1190
+ }
1191
+ buf += ch;
1192
+ vis++;
1193
+ }
1194
+ } else {
1195
+ if (vis + wordVis > available && vis > 0) {
1196
+ lines.push(indent + buf.trimEnd());
1197
+ buf = "";
1198
+ vis = 0;
1199
+ }
1200
+ buf += word;
1201
+ vis += wordVis;
1202
+ }
1203
+ word = "";
1204
+ wordVis = 0;
1205
+ };
1206
+ for (const ch of [...text]) {
1207
+ if (/\s/.test(ch)) {
1208
+ flushWord();
1209
+ if (vis < available && vis > 0) {
1210
+ buf += " ";
1211
+ vis++;
1212
+ }
1213
+ } else {
1214
+ word += ch;
1215
+ wordVis++;
1216
+ }
1217
+ }
1218
+ flushWord();
1219
+ if (buf) lines.push(indent + buf.trimEnd());
1220
+ return lines;
1221
+ }
1222
+ const colors = {
1223
+ reset: "\x1B[0m",
1224
+ bold: "\x1B[1m",
1225
+ brightRed: "\x1B[91m",
1226
+ brightGreen: "\x1B[92m"
1227
+ };
1228
+ class Logger {
1229
+ previousLines = [];
1230
+ showPrompt = true;
1231
+ prompt;
1232
+ errorPrompt;
1233
+ constructor(options = {}) {
1234
+ this.prompt = `${colors.bold}${options.promptColor ?? colors.brightGreen}> ${colors.reset}`;
1235
+ this.errorPrompt = `${options.errorPromptColor ?? colors.brightRed}> ${colors.reset}`;
1236
+ }
1237
+ // โ”€โ”€โ”€ Utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1238
+ visibleWidth(str) {
1239
+ return [...str.replace(ANSI_FULL_REGEX, "")].length;
1240
+ }
1241
+ clearLine() {
1242
+ process.stdout.write("\r\x1B[K");
1243
+ }
1244
+ writeLine(line, color = "") {
1245
+ this.clearLine();
1246
+ process.stdout.write(color + line + colors.reset);
1247
+ if (this.visibleWidth(line) >= MAX_WIDTH) {
1248
+ process.stdout.write("\n");
1249
+ }
1250
+ }
1251
+ addLine(text, opts = {}) {
1252
+ this.previousLines.push({
1253
+ text,
1254
+ isRaw: opts.isRaw,
1255
+ hasPrompt: opts.hasPrompt ?? this.showPrompt
1256
+ });
1257
+ if (this.previousLines.length > 200) {
1258
+ this.previousLines.splice(0, 100);
1259
+ }
1260
+ }
1261
+ // โ”€โ”€โ”€ REFORMAT (RESTORED) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1262
+ reformat(text, opts) {
1263
+ const { width, align = "left", padChar = " " } = opts;
1264
+ const normalized = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
1265
+ const lines = [];
1266
+ let buf = "";
1267
+ let vis = 0;
1268
+ const tokens = normalized.split(
1269
+ /(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)))/g
1270
+ );
1271
+ for (const token of tokens) {
1272
+ if (ANSI_FULL_REGEX.test(token)) {
1273
+ buf += token;
1274
+ continue;
1275
+ }
1276
+ for (const ch of [...token]) {
1277
+ if (vis >= width) {
1278
+ lines.push(this.applyPadding(buf, vis, width, align, padChar));
1279
+ buf = "";
1280
+ vis = 0;
1281
+ }
1282
+ buf += ch;
1283
+ vis++;
1284
+ }
1285
+ }
1286
+ if (buf) {
1287
+ lines.push(this.applyPadding(buf, vis, width, align, padChar));
1288
+ }
1289
+ return lines;
1290
+ }
1291
+ applyPadding(text, visible, width, align, padChar) {
1292
+ const pad = Math.max(0, width - visible);
1293
+ switch (align) {
1294
+ case "right":
1295
+ return padChar.repeat(pad) + text;
1296
+ case "center": {
1297
+ const left = Math.floor(pad / 2);
1298
+ const right = pad - left;
1299
+ return padChar.repeat(left) + text + padChar.repeat(right);
1300
+ }
1301
+ default:
1302
+ return text + padChar.repeat(pad);
1303
+ }
1304
+ }
1305
+ // โ”€โ”€โ”€ Printing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1306
+ print(msg) {
1307
+ const lines = wrapLine(
1308
+ this.showPrompt ? this.prompt + msg : msg,
1309
+ MAX_WIDTH,
1310
+ 0,
1311
+ "word"
1312
+ );
1313
+ for (let i = 0; i < lines.length; i++) {
1314
+ this.writeLine(lines[i]);
1315
+ if (i < lines.length - 1) process.stdout.write("\n");
1316
+ this.addLine(lines[i]);
1317
+ }
1318
+ this.showPrompt = false;
1319
+ return true;
1320
+ }
1321
+ println(msg = "") {
1322
+ if (msg) this.print(msg);
1323
+ process.stdout.write("\n");
1324
+ this.addLine("");
1325
+ this.showPrompt = true;
1326
+ return true;
1327
+ }
1328
+ printRaw(line) {
1329
+ process.stdout.write(line);
1330
+ this.addLine(line, { isRaw: true });
1331
+ return true;
1332
+ }
1333
+ printlnRaw(line = "") {
1334
+ this.printRaw(line);
1335
+ process.stdout.write("\n");
1336
+ this.addLine("", { isRaw: true });
1337
+ return true;
1338
+ }
1339
+ error(msg) {
1340
+ const lines = wrapLine(
1341
+ this.showPrompt ? this.errorPrompt + msg : msg,
1342
+ MAX_WIDTH,
1343
+ 0,
1344
+ "word"
1345
+ );
1346
+ for (let i = 0; i < lines.length; i++) {
1347
+ this.writeLine(lines[i], colors.brightRed);
1348
+ if (i < lines.length - 1) process.stdout.write("\n");
1349
+ this.addLine(lines[i]);
1350
+ }
1351
+ process.stdout.write("\n");
1352
+ this.showPrompt = true;
1353
+ return true;
1354
+ }
1355
+ // โ”€โ”€โ”€ History โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1356
+ clearHistory() {
1357
+ this.previousLines = [];
1358
+ }
1359
+ getHistory() {
1360
+ return [...this.previousLines];
1361
+ }
1362
+ }
1363
+ const logger = new Logger();
1364
+ class ConfigManager {
1365
+ static ensureConfigExists(configPath) {
1366
+ const jsonPath = norm(configPath || path.resolve(process.cwd(), "testify.json"));
1367
+ const cleaner = new JSONCleaner();
1368
+ if (fs.existsSync(jsonPath)) {
1369
+ try {
1370
+ return cleaner.parse(fs.readFileSync(jsonPath, "utf-8"));
1371
+ } catch (error) {
1372
+ logger.error(`โŒ Failed to parse existing testify.json ${error}`);
1373
+ return {};
1374
+ }
1375
+ }
1376
+ const defaultConfig = this.createDefaultConfig();
1377
+ try {
1378
+ fs.writeFileSync(jsonPath, JSON.stringify(defaultConfig, null, 2));
1379
+ logger.println(`๐Ÿ†• Created default test runner config at ${jsonPath}`);
1380
+ } catch (error) {
1381
+ logger.error(`โŒ Failed to create default testify.json ${error}`);
1382
+ }
1383
+ return defaultConfig;
1384
+ }
1385
+ static createDefaultConfig() {
1386
+ const configDir = norm(process.cwd());
1387
+ const rel = (p) => {
1388
+ const r = path.relative(configDir, p);
1389
+ return r === "" ? "." : norm(r);
1390
+ };
1391
+ const srcAbsolute = path.join(configDir, "src");
1392
+ const testAbsolute = path.join(configDir, "tests");
1393
+ const outAbsolute = path.join(configDir, "dist/.vite-jasmine-build/");
1394
+ return {
1395
+ srcDirs: [rel(srcAbsolute)],
1396
+ // ["./src"]
1397
+ testDirs: [rel(testAbsolute)],
1398
+ // ["./tests"]
1399
+ exclude: ["**/node_modules/**"],
1400
+ preserveOutputs: false,
1401
+ outDir: rel(outAbsolute),
1402
+ // "./dist/.vite-jasmine-build"
1403
+ browser: "chrome",
1404
+ headless: false,
1405
+ coverage: false,
1406
+ port: 8888,
1407
+ viteBuildOptions: {
1408
+ target: "es2022",
1409
+ sourcemap: true,
1410
+ minify: false,
1411
+ preserveModules: false,
1412
+ preserveModulesRoot: rel(configDir)
1413
+ // "./"
1414
+ },
1415
+ jasmineConfig: {
1416
+ env: { stopSpecOnExpectationFailure: false, random: true, seed: 0, timeout: 12e4 }
1417
+ },
1418
+ htmlOptions: {
1419
+ title: "Jasmine Tests Runner"
1420
+ },
1421
+ suppressConsoleLogs: false
1422
+ };
1423
+ }
1424
+ static initViteJasmineConfig(configPath) {
1425
+ const jsonPath = norm(configPath || path.resolve(process.cwd(), "testify.json"));
1426
+ if (fs.existsSync(jsonPath)) {
1427
+ logger.println(`โš ๏ธ Config already exists at ${jsonPath}`);
1428
+ return;
1429
+ }
1430
+ const defaultConfig = this.createDefaultConfig();
1431
+ fs.writeFileSync(jsonPath, JSON.stringify(defaultConfig, null, 2));
1432
+ logger.println(`โœ… Generated default Vite Jasmine config at ${jsonPath}`);
1433
+ }
1434
+ static loadViteJasmineBrowserConfig(configPath) {
1435
+ return this.ensureConfigExists(configPath);
1436
+ }
1437
+ }
1438
+ class BrowserManager {
1439
+ constructor(config) {
1440
+ this.config = config;
1441
+ }
1442
+ playwright = null;
1443
+ currentBrowser = null;
1444
+ currentPage = null;
1445
+ getPlaywright() {
1446
+ if (!this.playwright) {
1447
+ this.playwright = require2("playwright");
1448
+ }
1449
+ return this.playwright;
1450
+ }
1451
+ async checkBrowser(browserName) {
1452
+ try {
1453
+ const playwright = this.getPlaywright();
1454
+ let browser = null;
1455
+ switch (browserName.toLowerCase()) {
1456
+ case "chromium":
1457
+ case "chrome":
1458
+ browser = playwright.chromium;
1459
+ break;
1460
+ case "firefox":
1461
+ browser = playwright.firefox;
1462
+ break;
1463
+ case "webkit":
1464
+ case "safari":
1465
+ browser = playwright.webkit;
1466
+ break;
1467
+ default:
1468
+ logger.println(`โš ๏ธ Unknown browser "${browserName}", falling back to Node.js mode`);
1469
+ return null;
1470
+ }
1471
+ return browser;
1472
+ } catch (err) {
1473
+ if (err.code === "MODULE_NOT_FOUND") {
1474
+ logger.println(`โ„น๏ธ Playwright not installed. Browser "${browserName}" not available.`);
1475
+ logger.println(`๐Ÿ’ก Tip: Install Playwright to enable browser testing:
1476
+ npm install playwright`);
1477
+ } else {
1478
+ logger.error(`โŒ Browser execution failed for "${browserName}": ${err.message}`);
1479
+ }
1480
+ return null;
1481
+ }
1482
+ }
1483
+ async runHeadlessBrowserTests(browserType, port) {
1484
+ const browser = await browserType.launch({
1485
+ headless: true,
1486
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
1487
+ });
1488
+ const page = await browser.newPage();
1489
+ page.setDefaultTimeout(0);
1490
+ let interrupted = false;
1491
+ const interruptError = new Error("Interrupted");
1492
+ let interruptReject = null;
1493
+ const interruptPromise = new Promise((_, reject) => {
1494
+ interruptReject = reject;
1495
+ });
1496
+ const abortRun = () => {
1497
+ interrupted = true;
1498
+ if (interruptReject) interruptReject(interruptError);
1499
+ if (!page.isClosed()) {
1500
+ void page.close().catch(() => {
1501
+ });
1502
+ }
1503
+ void browser.close().catch(() => {
1504
+ });
1505
+ };
1506
+ const sigintHandler = () => abortRun();
1507
+ const sigtermHandler = () => abortRun();
1508
+ process.once("SIGINT", sigintHandler);
1509
+ process.once("SIGTERM", sigtermHandler);
1510
+ page.on("console", (msg) => {
1511
+ const text = msg.text();
1512
+ const type = msg.type();
1513
+ if (text.match(/error|failed/i)) {
1514
+ if (type === "error") logger.error(`BROWSER ERROR: ${text}`);
1515
+ else if (type === "warn") logger.println(`BROWSER WARN: ${text}`);
1516
+ }
1517
+ });
1518
+ page.on("pageerror", (error) => logger.error(`โŒ Page error: ${error.message}`));
1519
+ page.on("requestfailed", (request) => logger.error(`โŒ Request failed: ${request.url()}, ${request.failure()?.errorText}`));
1520
+ logger.println("๐ŸŒ Navigating to test page...");
1521
+ await page.goto(`http://localhost:${port}/index.html`, { waitUntil: "networkidle0", timeout: 12e4 });
1522
+ try {
1523
+ await Promise.race([
1524
+ page.waitForFunction(() => window.jasmineFinished === true, {
1525
+ timeout: this.config.jasmineConfig?.env?.timeout ?? 12e4
1526
+ }),
1527
+ interruptPromise
1528
+ ]);
1529
+ await new Promise((resolve) => setTimeout(resolve, 500));
1530
+ await browser.close();
1531
+ return true;
1532
+ } catch (error) {
1533
+ if (interrupted || error === interruptError) {
1534
+ logger.printRaw("\n\n");
1535
+ logger.println("๐Ÿ›‘ Tests aborted by user (Ctrl+C)");
1536
+ await browser.close();
1537
+ return false;
1538
+ }
1539
+ logger.error(`โŒ Test execution failed: ${error}`);
1540
+ await browser.close();
1541
+ throw error;
1542
+ } finally {
1543
+ process.removeListener("SIGINT", sigintHandler);
1544
+ process.removeListener("SIGTERM", sigtermHandler);
1545
+ }
1546
+ }
1547
+ async openBrowser(port, onBrowserClose, options) {
1548
+ let browserName = this.config.browser || "chrome";
1549
+ const url = `http://localhost:${port}/index.html`;
1550
+ try {
1551
+ const playwright = this.getPlaywright();
1552
+ let browserType;
1553
+ switch (browserName.toLowerCase()) {
1554
+ case "chrome":
1555
+ case "chromium":
1556
+ browserType = playwright.chromium;
1557
+ break;
1558
+ case "firefox":
1559
+ browserType = playwright.firefox;
1560
+ break;
1561
+ case "webkit":
1562
+ case "safari":
1563
+ browserType = playwright.webkit;
1564
+ break;
1565
+ default:
1566
+ logger.println(`โš ๏ธ Unknown browser "${browserName}", using Chrome instead`);
1567
+ browserType = playwright.chromium;
1568
+ browserName = "chrome";
1569
+ }
1570
+ if (!browserType) {
1571
+ logger.println(`โŒ Browser "${browserName}" is not installed.`);
1572
+ logger.println(`๐Ÿ’ก Tip: Install it by running: npx playwright install ${browserName.toLowerCase()}`);
1573
+ return;
1574
+ }
1575
+ logger.println(`๐ŸŒ Opening ${browserName} browser...`);
1576
+ const browser = await browserType.launch({
1577
+ headless: this.config.headless,
1578
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
1579
+ });
1580
+ const page = await browser.newPage();
1581
+ this.currentBrowser = browser;
1582
+ this.currentPage = page;
1583
+ await page.goto(url);
1584
+ const exitOnClose = options?.exitOnClose !== false;
1585
+ page.on("close", async () => {
1586
+ if (onBrowserClose) {
1587
+ await onBrowserClose();
1588
+ }
1589
+ this.clearBrowserState();
1590
+ if (exitOnClose) {
1591
+ process.exit(0);
1592
+ }
1593
+ });
1594
+ } catch (error) {
1595
+ if (error.code === "MODULE_NOT_FOUND") {
1596
+ logger.println(`โ„น๏ธ Playwright not installed. Please open browser manually: ${url}`);
1597
+ logger.println(`๐Ÿ’ก Tip: Install Playwright to enable automatic browser opening:
1598
+ npm install playwright`);
1599
+ } else {
1600
+ logger.error(`โŒ Failed to open browser: ${error.message}`);
1601
+ logger.println(`๐Ÿ’ก Please open browser manually: ${url}`);
1602
+ }
1603
+ }
1604
+ }
1605
+ clearBrowserState() {
1606
+ this.currentPage = null;
1607
+ this.currentBrowser = null;
1608
+ }
1609
+ async closeBrowser() {
1610
+ if (!this.currentBrowser && !this.currentPage) {
1611
+ return;
1612
+ }
1613
+ try {
1614
+ if (this.currentBrowser) {
1615
+ await this.currentBrowser.close();
1616
+ } else if (this.currentPage && !this.currentPage.isClosed()) {
1617
+ await this.currentPage.close();
1618
+ }
1619
+ } catch (error) {
1620
+ logger.error(`โŒ Failed to close browser: ${error?.message ?? error}`);
1621
+ } finally {
1622
+ this.clearBrowserState();
1623
+ }
1624
+ }
1625
+ }
1626
+ class FileDiscoveryService {
1627
+ constructor(config) {
1628
+ this.config = config;
1629
+ }
1630
+ getSrcDirConfigs() {
1631
+ const srcDirs = Array.isArray(this.config.srcDirs) ? this.config.srcDirs : [this.config.srcDirs];
1632
+ if (srcDirs.filter(Boolean).length === 0) return ["./src"];
1633
+ return srcDirs.filter(Boolean);
1634
+ }
1635
+ getTestDirConfigs() {
1636
+ const testDirs = Array.isArray(this.config.testDirs) ? this.config.testDirs : [this.config.testDirs];
1637
+ if (testDirs.filter(Boolean).length === 0) return ["./tests"];
1638
+ return testDirs.filter(Boolean);
1639
+ }
1640
+ async scanDir(dir, pattern, exclude = []) {
1641
+ const cleanPattern = pattern.startsWith("/") || pattern.startsWith("**") ? pattern : `/${pattern}`;
1642
+ const basePath = norm(path.join(dir, cleanPattern)).replace(/^\//, "");
1643
+ try {
1644
+ let files = await glob(basePath, { absolute: true, ignore: exclude });
1645
+ return files.map((s) => norm(s));
1646
+ } catch (error) {
1647
+ logger.error(`โŒ Error discovering files: ${error}`);
1648
+ throw new Error("Failed to discover source and test files");
1649
+ }
1650
+ }
1651
+ async filterExistingFiles(paths) {
1652
+ const existingFiles = [];
1653
+ await Promise.all(
1654
+ paths.map(async (filePath) => {
1655
+ const normalizedPath = norm(filePath);
1656
+ try {
1657
+ await fs$1.access(normalizedPath);
1658
+ existingFiles.push(normalizedPath);
1659
+ } catch {
1660
+ }
1661
+ })
1662
+ );
1663
+ return existingFiles;
1664
+ }
1665
+ async discoverSources() {
1666
+ try {
1667
+ const defaultSrcExclude = ["**/node_modules/**", "**/*.spec.*"];
1668
+ const defaultTestExclude = ["**/node_modules/**"];
1669
+ const sharedExclude = this.config.exclude ?? [];
1670
+ const srcDirs = this.getSrcDirConfigs();
1671
+ const testDirs = this.getTestDirConfigs();
1672
+ const srcFiles = [];
1673
+ for (const inc of srcDirs) {
1674
+ const exclude = [...defaultSrcExclude, ...sharedExclude];
1675
+ const files = await this.scanDir(norm(inc), "/**/*.{ts,js,mjs}", exclude);
1676
+ srcFiles.push(...files);
1677
+ }
1678
+ const specFiles = [];
1679
+ for (const inc of testDirs) {
1680
+ const exclude = [...defaultTestExclude, ...sharedExclude];
1681
+ const files = await this.scanDir(norm(inc), "/**/*.spec.{ts,js,mjs}", exclude);
1682
+ specFiles.push(...files);
1683
+ }
1684
+ return { srcFiles: [...new Set(srcFiles)], specFiles: [...new Set(specFiles)] };
1685
+ } catch (error) {
1686
+ logger.error(`โŒ Error discovering files: ${error}`);
1687
+ throw new Error("Failed to discover source and test files");
1688
+ }
1689
+ }
1690
+ getOutputName(filePath) {
1691
+ const srcDirs = this.getSrcDirConfigs();
1692
+ const testDirs = this.getTestDirConfigs();
1693
+ const normalizedPath = norm(path.resolve(filePath));
1694
+ const resolveDirs = (dirs) => dirs.map((dir) => norm(path.resolve(dir)));
1695
+ const normalizedSrcDirs = resolveDirs(srcDirs);
1696
+ const normalizedTestDirs = resolveDirs(testDirs);
1697
+ if (!normalizedSrcDirs.length) {
1698
+ normalizedSrcDirs.push(norm(path.resolve("./src")));
1699
+ }
1700
+ if (!normalizedTestDirs.length) {
1701
+ normalizedTestDirs.push(norm(path.resolve("./tests")));
1702
+ }
1703
+ const matchDir = (dirs) => {
1704
+ for (const candidate of dirs) {
1705
+ if (normalizedPath === candidate || normalizedPath.startsWith(`${candidate}/`)) {
1706
+ return candidate;
1707
+ }
1708
+ }
1709
+ return null;
1710
+ };
1711
+ const baseTest = matchDir(normalizedTestDirs);
1712
+ const baseSrc = matchDir(normalizedSrcDirs) ?? normalizedSrcDirs[0];
1713
+ const base = baseTest ?? baseSrc;
1714
+ const relativePath = path.relative(base, normalizedPath);
1715
+ const relativeNormalized = norm(relativePath);
1716
+ const relativeWithoutExt = relativeNormalized.replace(/\.(ts|js|mjs)$/, "");
1717
+ const sanitizeSegment = (segment) => {
1718
+ if (segment === "..") return "up";
1719
+ if (segment === ".") return "dot";
1720
+ return segment;
1721
+ };
1722
+ const segments = relativeWithoutExt.split("/").filter(Boolean);
1723
+ const sanitized = segments.length > 0 ? segments.map(sanitizeSegment).join("_") : sanitizeSegment(path.basename(relativeWithoutExt) || "index");
1724
+ return `${sanitized}.js`;
1725
+ }
1726
+ }
1727
+ class HtmlGenerator {
1728
+ constructor(fileDiscovery, config) {
1729
+ this.fileDiscovery = fileDiscovery;
1730
+ this.config = config;
1731
+ }
1732
+ async generateHtmlFile() {
1733
+ const htmlDir = this.config.outDir;
1734
+ if (!fs.existsSync(htmlDir)) {
1735
+ fs.mkdirSync(htmlDir, { recursive: true });
1736
+ }
1737
+ const builtFiles = fs.readdirSync(htmlDir).filter((f) => f.endsWith(".js")).sort();
1738
+ if (builtFiles.length === 0) {
1739
+ logger.println("โš ๏ธ No JS files found for HTML generation.");
1740
+ return;
1741
+ }
1742
+ const sourceFiles = builtFiles.filter((f) => !f.endsWith(".spec.js"));
1743
+ const specFiles = builtFiles.filter((f) => f.endsWith(".spec.js"));
1744
+ const imports = [...sourceFiles, ...specFiles].map((f) => `import "./${f}";`).join("\n ");
1745
+ const faviconTag = this.getFaviconTag();
1746
+ const htmlContent = this.generateHtmlTemplate(imports, faviconTag);
1747
+ const htmlPath = norm(path.join(htmlDir, "index.html"));
1748
+ fs.writeFileSync(htmlPath, htmlContent);
1749
+ logger.println(`๐Ÿ“„ Generated test page: ${norm(path.relative(this.config.outDir, htmlPath))}`);
1750
+ }
1751
+ async generateHtmlFileWithHmr() {
1752
+ const htmlDir = this.config.outDir;
1753
+ if (!fs.existsSync(htmlDir)) {
1754
+ fs.mkdirSync(htmlDir, { recursive: true });
1755
+ }
1756
+ const faviconTag = this.getFaviconTag();
1757
+ const htmlContent = this.generateHtmlTemplateWithHmr(faviconTag);
1758
+ const htmlPath = norm(path.join(htmlDir, "index.html"));
1759
+ fs.writeFileSync(htmlPath, htmlContent);
1760
+ console.log("๐Ÿ“„ Generated HMR-enabled test page:", norm(path.relative(this.config.outDir, htmlPath)));
1761
+ }
1762
+ getFaviconTag() {
1763
+ const __filename2 = norm(fileURLToPath(import.meta.url));
1764
+ const __dirname2 = norm(path.dirname(__filename2));
1765
+ const faviconPath = path.resolve(__dirname2, "../assets/favicon.ico");
1766
+ if (fs.existsSync(faviconPath)) {
1767
+ const faviconData = fs.readFileSync(faviconPath);
1768
+ const faviconBase64 = faviconData.toString("base64");
1769
+ return `<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,${faviconBase64}">`;
1770
+ } else {
1771
+ logger.println(`โš ๏ธ Favicon not found at ${faviconPath}, using default <link>`);
1772
+ return `<link rel="icon" href="favicon.ico" type="image/x-icon" />`;
1773
+ }
1774
+ }
1775
+ generateHtmlTemplate(imports, faviconTag) {
1776
+ return `<!DOCTYPE html>
1777
+ <html>
1778
+ <head>
1779
+ <meta charset="UTF-8">
1780
+ ${faviconTag}
1781
+ <title>${this.config.htmlOptions?.title || "Jasmine Tests Runner"}</title>
1782
+ <link rel="stylesheet" href="/node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
1783
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/jasmine.js"><\/script>
1784
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"><\/script>
1785
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/boot0.js"><\/script>
1786
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/boot1.js"><\/script>
1787
+ <script type="module">
1788
+ ${this.getWebSocketEventForwarderScript()}
1789
+
1790
+ const forwarder = new WebSocketEventForwarder();
1791
+ forwarder.connect();
1792
+ jasmine.getEnv().addReporter(forwarder);
1793
+
1794
+ ${imports}
1795
+ <\/script>
1796
+ </head>
1797
+ <body>
1798
+ <div class="jasmine_html-reporter"></div>
1799
+ </body>
1800
+ </html>`;
1801
+ }
1802
+ generateHtmlTemplateWithHmr(faviconTag) {
1803
+ return `<!DOCTYPE html>
1804
+ <html>
1805
+ <head>
1806
+ <meta charset="UTF-8">
1807
+ ${faviconTag}
1808
+ <title>${this.config.htmlOptions?.title || "Jasmine Tests Runner (HMR)"}</title>
1809
+ <link rel="stylesheet" href="/node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
1810
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/jasmine.js"><\/script>
1811
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"><\/script>
1812
+
1813
+ <script>
1814
+ ${this.getJasminePatchScript()}
1815
+
1816
+ ${this.getWebSocketEventForwarderScript()}
1817
+
1818
+ ${this.getHmrClientScript()}
1819
+
1820
+ // Initialize everything after Jasmine is loaded
1821
+ (function initAfterJasmine() {
1822
+ if (!window.jasmineRequire) {
1823
+ return setTimeout(initAfterJasmine, 10);
1824
+ }
1825
+
1826
+ const script = document.createElement('script');
1827
+ script.src = '/node_modules/jasmine-core/lib/jasmine-core/boot0.js';
1828
+
1829
+ script.onload = () => {
1830
+ // Add the WebSocket forwarder as a reporter
1831
+ const forwarder = new WebSocketEventForwarder();
1832
+ forwarder.connect();
1833
+ jasmine.getEnv().addReporter(forwarder);
1834
+
1835
+ ${this.getRuntimeHelpersScript()}
1836
+ };
1837
+
1838
+ script.onerror = (err) => {
1839
+ console.error('Failed to load boot0.js:', err);
1840
+ };
1841
+
1842
+ document.head.appendChild(script);
1843
+ })();
1844
+ <\/script>
1845
+ </head>
1846
+ <body>
1847
+ <div class="jasmine_html-reporter"></div>
1848
+ </body>
1849
+ </html>`;
1850
+ }
1851
+ getJasminePatchScript() {
1852
+ return `
1853
+ // Patch Jasmine before boot to add metadata backlinks
1854
+ (function patchJasmineBeforeBoot() {
1855
+ if (!window.jasmineRequire) {
1856
+ return setTimeout(patchJasmineBeforeBoot, 10);
1857
+ }
1858
+
1859
+ const j$ = jasmineRequire.core(jasmineRequire);
1860
+ const OriginalSuiteFactory = jasmineRequire.Suite || j$.Suite || null;
1861
+ const OriginalEnvFactory = jasmineRequire.Env || j$.Env || null;
1862
+ const root = window.jasmineRequire || jasmineRequire;
1863
+
1864
+ // Helper to attach metadata backref
1865
+ function attachMetadataBackref(suite) {
1866
+ if (!suite || !suite.metadata) return;
1867
+ try {
1868
+ if (!suite.metadata.__suite) {
1869
+ Object.defineProperty(suite.metadata, '__suite', {
1870
+ value: suite,
1871
+ enumerable: false,
1872
+ configurable: true,
1873
+ writable: false
1874
+ });
1875
+ }
1876
+ } catch (e) {
1877
+ // Ignore errors
1878
+ }
1879
+ }
1880
+
1881
+ // Recursively attach backlinks
1882
+ function attachMetadataBackrefsRecursive(suite) {
1883
+ try {
1884
+ attachMetadataBackref(suite);
1885
+ if (Array.isArray(suite.children)) {
1886
+ suite.children.forEach(attachMetadataBackrefsRecursive);
1887
+ }
1888
+ } catch (e) {
1889
+ // Ignore errors
1890
+ }
1891
+ }
1892
+
1893
+ // Patch Suite factory
1894
+ if (OriginalSuiteFactory) {
1895
+ root.Suite = function(j$local) {
1896
+ const OriginalSuite = OriginalSuiteFactory(j$local);
1897
+
1898
+ return class PatchedSuite extends OriginalSuite {
1899
+ constructor(attrs) {
1900
+ super(attrs);
1901
+ attachMetadataBackref(this);
1902
+ }
1903
+ };
1904
+ };
1905
+ }
1906
+
1907
+ // Patch Env factory
1908
+ if (OriginalEnvFactory) {
1909
+ root.Env = function(j$local) {
1910
+ const OriginalEnv = OriginalEnvFactory(j$local);
1911
+
1912
+ return class PatchedEnv extends OriginalEnv {
1913
+ constructor(attrs) {
1914
+ super(attrs);
1915
+ try {
1916
+ if (this.topSuite) {
1917
+ window.__jasmine_real_topSuite = this.topSuite;
1918
+ attachMetadataBackrefsRecursive(this.topSuite);
1919
+ }
1920
+ } catch (e) {
1921
+ // Ignore errors
1922
+ }
1923
+ }
1924
+ };
1925
+ };
1926
+ }
1927
+
1928
+ // Define loadSpecs function in global scope
1929
+ window.loadSpecs = async function(srcFiles, specFiles) {
1930
+ // Wait for HMRClient
1931
+ let attempts = 0;
1932
+ while (!window.HMRClient && attempts < 100) {
1933
+ await new Promise(resolve => setTimeout(resolve, 50));
1934
+ attempts++;
1935
+ }
1936
+
1937
+ if (!window.HMRClient) {
1938
+ console.error('โŒ HMRClient not available after waiting');
1939
+ return;
1940
+ }
1941
+
1942
+ console.log('๐Ÿ“ฆ Loading spec files dynamically...');
1943
+
1944
+ try {
1945
+ // Load source files first
1946
+ for (const file of srcFiles) {
1947
+ await import('/' + file);
1948
+ }
1949
+
1950
+ // Then load spec files with file path tracking
1951
+ for (const file of specFiles) {
1952
+ const module = await import('/' + file);
1953
+
1954
+ if (window.HMRClient?.attachFilePathToSuites) {
1955
+ await window.HMRClient.attachFilePathToSuites(file, module);
1956
+ }
1957
+ }
1958
+
1959
+ console.log('โœ… All specs loaded and tagged with file paths');
1960
+ } catch (err) {
1961
+ console.error('โŒ Failed to load specs:', err);
1962
+ }
1963
+ };
1964
+ })();
1965
+ `;
1966
+ }
1967
+ getWebSocketEventForwarderScript() {
1968
+ const seed = this.config.jasmineConfig?.env?.seed ?? 0;
1969
+ const random = this.config.jasmineConfig?.env?.random ?? false;
1970
+ return `
1971
+ function WebSocketEventForwarder() {
1972
+ this.ws = null;
1973
+ this.connected = false;
1974
+ this.messageQueue = [];
1975
+
1976
+ const self = this;
1977
+
1978
+ this.connect = function () {
1979
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1980
+ const wsUrl = protocol + '//' + window.location.host;
1981
+
1982
+ self.ws = new WebSocket(wsUrl);
1983
+
1984
+ self.ws.onopen = () => {
1985
+ self.connected = true;
1986
+ console.log('WebSocket connected to', wsUrl);
1987
+
1988
+ self.send({
1989
+ type: 'userAgent',
1990
+ data: {
1991
+ userAgent: navigator.userAgent,
1992
+ appName: navigator.appName,
1993
+ appVersion: navigator.appVersion,
1994
+ platform: navigator.platform,
1995
+ vendor: navigator.vendor,
1996
+ language: navigator.language,
1997
+ languages: navigator.languages,
1998
+ orderedSuites: self.getOrderedSuites(${seed}, ${random}).map(suite => ({
1999
+ id: suite.id,
2000
+ description: suite.description,
2001
+ fullName: suite.getFullName ? suite.getFullName() : suite.description
2002
+ })),
2003
+ orderedSpecs: self.getOrderedSpecs(${seed}, ${random}).map(spec => ({
2004
+ id: spec.id,
2005
+ description: spec.description,
2006
+ fullName: spec.getFullName ? spec.getFullName() : spec.description
2007
+ }))
2008
+ },
2009
+ timestamp: Date.now()
2010
+ });
2011
+
2012
+ while (self.messageQueue.length > 0) {
2013
+ const msg = self.messageQueue.shift();
2014
+ self.send(msg);
2015
+ }
2016
+ };
2017
+
2018
+ self.ws.onclose = () => {
2019
+ self.connected = false;
2020
+ console.log('WebSocket disconnected');
2021
+ setTimeout(() => self.connect(), 1000);
2022
+ };
2023
+
2024
+ self.ws.onerror = (err) => {
2025
+ self.connected = false;
2026
+ console.error('WebSocket error:', err);
2027
+ };
2028
+
2029
+ self.ws.onmessage = async (event) => {
2030
+ try {
2031
+ const message = JSON.parse(event.data);
2032
+ if (window.HMRClient && (message.type === 'hmr:connected' || message.type === 'hmr:update')) {
2033
+ await window.HMRClient.handleMessage(message);
2034
+ }
2035
+ } catch (err) {
2036
+ console.error('Failed to handle WebSocket message:', err);
2037
+ }
2038
+ };
2039
+ };
2040
+
2041
+ this.send = function (msg) {
2042
+ if (self.connected && self.ws && self.ws.readyState === WebSocket.OPEN) {
2043
+ try {
2044
+ self.ws.send(JSON.stringify(msg));
2045
+ } catch (err) {
2046
+ console.error('Failed to send WebSocket message:', err);
2047
+ }
2048
+ } else {
2049
+ self.messageQueue.push(msg);
2050
+ }
2051
+ };
2052
+
2053
+ this.getAllSpecs = function () {
2054
+ const allSpecs = [];
2055
+ function collect(suite) {
2056
+ suite.children.forEach((child) => {
2057
+ if (child.children && child.children.length > 0) {
2058
+ collect(child);
2059
+ } else {
2060
+ allSpecs.push(child);
2061
+ }
2062
+ });
2063
+ }
2064
+
2065
+ const env = jasmine?.getEnv?.();
2066
+ if (env) collect(env.topSuite());
2067
+ return allSpecs;
2068
+ };
2069
+
2070
+ this.getAllSuites = function () {
2071
+ const allSuites = [];
2072
+ function collect(suite) {
2073
+ allSuites.push(suite);
2074
+ suite.children.forEach((child) => {
2075
+ if (child.children && child.children.length > 0) {
2076
+ collect(child);
2077
+ }
2078
+ });
2079
+ }
2080
+
2081
+ const env = jasmine?.getEnv?.();
2082
+ if (env) collect(env.topSuite());
2083
+ return allSuites;
2084
+ };
2085
+
2086
+ this.getOrderedSpecs = function (seed, random) {
2087
+ const allSpecs = self.getAllSpecs();
2088
+ if (!random) return allSpecs;
2089
+
2090
+ const OrderCtor = jasmine.Order;
2091
+ if (typeof OrderCtor === 'function') {
2092
+ try {
2093
+ const order = new OrderCtor({ random, seed });
2094
+ if (typeof order.sort === 'function') {
2095
+ return order.sort(allSpecs);
2096
+ }
2097
+ } catch (err) {
2098
+ console.error('Failed to create jasmine.Order:', err);
2099
+ }
2100
+ }
2101
+ return allSpecs;
2102
+ };
2103
+
2104
+ this.getOrderedSuites = function (seed, random) {
2105
+ const allSuites = self.getAllSuites();
2106
+ if (!random) return allSuites;
2107
+
2108
+ const OrderCtor = jasmine.Order;
2109
+ if (typeof OrderCtor === 'function') {
2110
+ try {
2111
+ const order = new OrderCtor({ random, seed });
2112
+ if (typeof order.sort === 'function') {
2113
+ return order.sort(allSuites);
2114
+ }
2115
+ } catch (err) {
2116
+ console.error('Failed to create jasmine.Order for suites:', err);
2117
+ }
2118
+ }
2119
+ return allSuites;
2120
+ };
2121
+
2122
+ // Jasmine reporter hooks
2123
+ this.jasmineStarted = function (config) {
2124
+ let orderedSpecs = [];
2125
+ let orderedSuites = [];
2126
+
2127
+ if (config.order) {
2128
+ const random = !!config.order.random;
2129
+ const seed = config.order.seed;
2130
+ orderedSpecs = self.getOrderedSpecs(seed, random);
2131
+ orderedSuites = self.getOrderedSuites(seed, random);
2132
+ }
2133
+
2134
+ self.send({
2135
+ type: 'jasmineStarted',
2136
+ data: config,
2137
+ timestamp: Date.now()
2138
+ });
2139
+ };
2140
+
2141
+ this.suiteStarted = function (suite) {
2142
+ self.send({
2143
+ type: 'suiteStarted',
2144
+ id: suite.id,
2145
+ description: suite.description,
2146
+ fullName: suite.fullName,
2147
+ timestamp: Date.now()
2148
+ });
2149
+ };
2150
+
2151
+ this.specStarted = function (spec) {
2152
+ self.send({
2153
+ type: 'specStarted',
2154
+ id: spec.id,
2155
+ description: spec.description,
2156
+ fullName: spec.fullName,
2157
+ timestamp: Date.now()
2158
+ });
2159
+ };
2160
+
2161
+ this.specDone = function (result) {
2162
+ self.send({
2163
+ type: 'specDone',
2164
+ ...result,
2165
+ timestamp: Date.now()
2166
+ });
2167
+ };
2168
+
2169
+ this.suiteDone = function (suite) {
2170
+ self.send({
2171
+ type: 'suiteDone',
2172
+ id: suite.id,
2173
+ description: suite.description,
2174
+ fullName: suite.fullName,
2175
+ timestamp: Date.now()
2176
+ });
2177
+ };
2178
+
2179
+ this.jasmineDone = function (result) {
2180
+ const coverage = globalThis.__coverage__;
2181
+ self.send({
2182
+ type: 'jasmineDone',
2183
+ ...result,
2184
+ coverage: coverage ? JSON.stringify(coverage) : null,
2185
+ timestamp: Date.now()
2186
+ });
2187
+
2188
+ window.jasmineFinished = true;
2189
+
2190
+ if (!window.HMRClient) {
2191
+ setTimeout(() => {
2192
+ if (self.ws) self.ws.close();
2193
+ }, 1000);
2194
+ }
2195
+ };
2196
+ }
2197
+ `;
2198
+ }
2199
+ getHmrClientScript() {
2200
+ return `
2201
+ // HMR Client Runtime
2202
+ window.HMRClient = (function() {
2203
+ const moduleRegistry = new Map();
2204
+
2205
+ function getEnv() {
2206
+ return window.jasmine?.getEnv?.();
2207
+ }
2208
+
2209
+ function setFilePath(obj, filePath) {
2210
+ if (!obj) return;
2211
+ try {
2212
+ Object.defineProperty(obj, '_filePath', {
2213
+ value: filePath,
2214
+ enumerable: false,
2215
+ configurable: true,
2216
+ writable: true
2217
+ });
2218
+ } catch (e) {
2219
+ obj._filePath = filePath;
2220
+ }
2221
+ }
2222
+
2223
+ async function attachFilePathToSuites(filePath, moduleExports) {
2224
+ const env = getEnv();
2225
+ if (!env) return;
2226
+
2227
+ const topSuite = env.topSuite().__suite || env.topSuite();
2228
+ if (!topSuite) return;
2229
+
2230
+ function tagSuites(suite) {
2231
+ if (!suite) return;
2232
+
2233
+ if (!suite._filePath) {
2234
+ setFilePath(suite, filePath);
2235
+ }
2236
+
2237
+ if (suite.metadata && !suite.metadata.__suite) {
2238
+ try {
2239
+ Object.defineProperty(suite.metadata, '__suite', {
2240
+ value: suite,
2241
+ enumerable: false,
2242
+ configurable: true,
2243
+ writable: false
2244
+ });
2245
+ } catch (e) {
2246
+ // Ignore
2247
+ }
2248
+ }
2249
+
2250
+ const children = suite.children || [];
2251
+ for (const ch of children) {
2252
+ tagSuites(ch);
2253
+ }
2254
+ }
2255
+
2256
+ tagSuites(topSuite);
2257
+ }
2258
+
2259
+ function detachFilePathSuites(filePath) {
2260
+ const env = getEnv();
2261
+ if (!env) return;
2262
+
2263
+ const topSuite = env.topSuite().__suite || env.topSuite();
2264
+ if (!topSuite) return;
2265
+
2266
+ function cleanSuite(suite) {
2267
+ if (!suite || !Array.isArray(suite.children)) return;
2268
+
2269
+ const keep = [];
2270
+
2271
+ for (const childWrapper of suite.children) {
2272
+ if (!childWrapper) continue;
2273
+
2274
+ const child = childWrapper;
2275
+
2276
+ if (child._filePath === filePath) {
2277
+ continue;
2278
+ }
2279
+
2280
+ if (child.children && Array.isArray(child.children)) {
2281
+ cleanSuite(child);
2282
+ }
2283
+
2284
+ keep.push(childWrapper);
2285
+ }
2286
+
2287
+ if (suite.removeChildren && suite.addChild) {
2288
+ suite.removeChildren();
2289
+ keep.forEach(item => suite.addChild(item));
2290
+ } else {
2291
+ suite.children = keep;
2292
+ }
2293
+
2294
+ if (Array.isArray(suite.specs)) {
2295
+ suite.specs = suite.specs.filter(spec => spec._filePath !== filePath);
2296
+ }
2297
+ }
2298
+
2299
+ cleanSuite(topSuite);
2300
+ console.log(\`๐Ÿงน Detached all suites/specs with _filePath: \${filePath}\`);
2301
+ }
2302
+
2303
+ async function hotUpdateSpec(filePath, moduleExports) {
2304
+ detachFilePathSuites(filePath);
2305
+ await attachFilePathToSuites(filePath, moduleExports);
2306
+ console.log('โœ… Hot updated Jasmine suites from:', filePath);
2307
+ }
2308
+
2309
+ async function handleMessage(message) {
2310
+ if (message.type === 'hmr:connected') {
2311
+ console.log('๐Ÿ”ฅ HMR enabled on server');
2312
+ if (window.loadSpecs) {
2313
+ await window.loadSpecs(message.srcFiles, message.specFiles);
2314
+ }
2315
+ return;
2316
+ }
2317
+
2318
+ if (message.type === 'hmr:update') {
2319
+ const update = message.data;
2320
+ if (!update) return;
2321
+
2322
+ if (update.type === 'full-reload') {
2323
+ console.log('๐Ÿ”„ Full reload required');
2324
+ location.reload();
2325
+ return;
2326
+ }
2327
+
2328
+ console.log('๐Ÿ”ฅ Hot updating:', update.path);
2329
+
2330
+ try {
2331
+ let newModule = null;
2332
+ if (update.content) {
2333
+ newModule = await import('/' + update.path + \`?t=\${Date.now()}\`);
2334
+ moduleRegistry.set(update.path, newModule);
2335
+ }
2336
+
2337
+ await hotUpdateSpec(update.path, newModule);
2338
+ console.log('โœ… HMR update applied:', update.path);
2339
+ } catch (err) {
2340
+ console.error('โŒ HMR update failed:', err);
2341
+ location.reload();
2342
+ }
2343
+ }
2344
+ }
2345
+
2346
+ return {
2347
+ handleMessage,
2348
+ attachFilePathToSuites,
2349
+ detachFilePathSuites,
2350
+ clearCache: (filePath) => {
2351
+ if (filePath) moduleRegistry.delete(filePath);
2352
+ else moduleRegistry.clear();
2353
+ }
2354
+ };
2355
+ })();
2356
+ `;
2357
+ }
2358
+ getRuntimeHelpersScript() {
2359
+ const stopOnSpecFailure = this.config.jasmineConfig?.env?.stopSpecOnExpectationFailure ?? false;
2360
+ const initialSeed = this.config.jasmineConfig?.env?.seed ?? 0;
2361
+ const initialRandom = this.config.jasmineConfig?.env?.random ?? false;
2362
+ return `
2363
+ (function(globalThis) {
2364
+ async function waitForJasmine(maxAttempts = 50, interval = 100) {
2365
+ return new Promise((resolve, reject) => {
2366
+ let attempts = 0;
2367
+
2368
+ function check() {
2369
+ if (globalThis.jasmine?.getEnv) {
2370
+ resolve(globalThis.jasmine.getEnv());
2371
+ } else if (attempts >= maxAttempts) {
2372
+ reject(new Error('Jasmine environment not found after waiting'));
2373
+ } else {
2374
+ attempts++;
2375
+ setTimeout(check, interval);
2376
+ }
2377
+ }
2378
+
2379
+ check();
2380
+ });
2381
+ }
2382
+
2383
+ async function init() {
2384
+ let env;
2385
+ try {
2386
+ env = await waitForJasmine();
2387
+ console.log('โœ… Jasmine environment found');
2388
+ } catch (error) {
2389
+ console.error('โš ๏ธ Jasmine environment not found:', error.message);
2390
+ return;
2391
+ }
2392
+
2393
+ let random = ${initialRandom};
2394
+ let seed = ${initialSeed};
2395
+
2396
+ env.configure({
2397
+ random,
2398
+ stopOnSpecFailure: ${stopOnSpecFailure},
2399
+ seed,
2400
+ autoCleanClosures: false
2401
+ });
2402
+
2403
+ function isSpec(child) {
2404
+ return child && typeof child.id === 'string' && !child.children;
2405
+ }
2406
+
2407
+ function isSuite(child) {
2408
+ return child && Array.isArray(child.children);
2409
+ }
2410
+
2411
+ function getAllSpecs() {
2412
+ const specs = [];
2413
+ const traverse = suite => {
2414
+ (suite.children || []).forEach(child => {
2415
+ if (isSpec(child)) specs.push(child);
2416
+ if (isSuite(child)) traverse(child);
2417
+ });
2418
+ };
2419
+ traverse(env.topSuite());
2420
+ return specs;
2421
+ }
2422
+
2423
+ function getAllSuites() {
2424
+ const suites = [];
2425
+ const traverse = suite => {
2426
+ suites.push(suite);
2427
+ (suite.children || []).forEach(child => {
2428
+ if (isSuite(child)) traverse(child);
2429
+ });
2430
+ };
2431
+ traverse(env.topSuite());
2432
+ return suites;
2433
+ }
2434
+
2435
+ function getOrderedSpecs(seed, random) {
2436
+ const all = getAllSpecs();
2437
+ if (!random) return all;
2438
+
2439
+ try {
2440
+ const order = new globalThis.jasmine.Order({ random, seed });
2441
+ return order.sort?.(all) ?? all;
2442
+ } catch {
2443
+ return all;
2444
+ }
2445
+ }
2446
+
2447
+ function getOrderedSuites(seed, random) {
2448
+ const all = getAllSuites();
2449
+ if (!random) return all;
2450
+
2451
+ try {
2452
+ const order = new globalThis.jasmine.Order({ random, seed });
2453
+ return order.sort?.(all) ?? all;
2454
+ } catch {
2455
+ return all;
2456
+ }
2457
+ }
2458
+
2459
+ globalThis.jasmine = {
2460
+ ...globalThis.jasmine,
2461
+ getAllSpecs,
2462
+ getAllSuites,
2463
+ getOrderedSpecs,
2464
+ getOrderedSuites
2465
+ };
2466
+
2467
+ let originalSpecFilter = null;
2468
+ let isExecuting = false;
2469
+
2470
+ const inBrowserReporter = {
2471
+ results: [],
2472
+ currentSpecIdSet: null,
2473
+
2474
+ jasmineStarted: function () {
2475
+ this.results = [];
2476
+ },
2477
+
2478
+ specStarted: function (config) {
2479
+ if (this.currentSpecIdSet?.has(config.id)) {
2480
+ console.log(\`โ–ถ๏ธ Running [\${config.id}]: \${config.description}\`);
2481
+ }
2482
+ },
2483
+
2484
+ specDone: function (result) {
2485
+ if (this.currentSpecIdSet?.has(result.id)) {
2486
+ this.results.push(result);
2487
+ const status = result.status.toUpperCase();
2488
+ console.log(\`[\${status}] \${result.description}\`);
2489
+
2490
+ result.failedExpectations?.forEach(f =>
2491
+ console.error('โŒ', f.message, f.stack ? '\\n' + f.stack : '')
2492
+ );
2493
+ }
2494
+ },
2495
+
2496
+ jasmineDone: () => {
2497
+ if (originalSpecFilter !== null) {
2498
+ env.configure({ specFilter: originalSpecFilter });
2499
+ }
2500
+ isExecuting = false;
2501
+ }
2502
+ };
2503
+
2504
+ env.addReporter(inBrowserReporter);
2505
+
2506
+ function resetEnvironment() {
2507
+ const resetNode = (node) => {
2508
+ if (node.result) {
2509
+ node.result = {
2510
+ status: 'pending',
2511
+ failedExpectations: [],
2512
+ passedExpectations: []
2513
+ };
2514
+ }
2515
+ node.children?.forEach(resetNode);
2516
+ };
2517
+
2518
+ resetNode(env.topSuite());
2519
+ }
2520
+
2521
+ async function executeSpecsByIds(specIds) {
2522
+ if (isExecuting) {
2523
+ console.warn('โš ๏ธ Execution already in progress. Please wait...');
2524
+ return [];
2525
+ }
2526
+
2527
+ return new Promise((resolve) => {
2528
+ isExecuting = true;
2529
+ inBrowserReporter.results = [];
2530
+ const specIdSet = new Set(specIds);
2531
+ inBrowserReporter.currentSpecIdSet = specIdSet;
2532
+
2533
+ if (originalSpecFilter === null) {
2534
+ originalSpecFilter = env.specFilter;
2535
+ }
2536
+
2537
+ resetEnvironment();
2538
+
2539
+ env.configure({
2540
+ random,
2541
+ seed,
2542
+ specFilter: (spec) => specIdSet.has(spec.id),
2543
+ autoCleanClosures: false
2544
+ });
2545
+
2546
+ const originalDone = inBrowserReporter.jasmineDone;
2547
+ inBrowserReporter.jasmineDone = () => {
2548
+ originalDone.call(inBrowserReporter);
2549
+ resolve(inBrowserReporter.results);
2550
+ inBrowserReporter.jasmineDone = originalDone;
2551
+ };
2552
+
2553
+ env.execute();
2554
+ });
2555
+ }
2556
+
2557
+ async function runTests(filters) {
2558
+ const allSpecs = getAllSpecs();
2559
+ const filterArr = Array.isArray(filters) ? filters : [filters];
2560
+ const matching = filterArr.length
2561
+ ? allSpecs.filter(s => filterArr.some(f =>
2562
+ f instanceof RegExp ? f.test(s.description) : s.id === f || s.description === f
2563
+ ))
2564
+ : allSpecs;
2565
+
2566
+ if (!matching.length) {
2567
+ console.warn('No matching specs found for:', filters);
2568
+ return [];
2569
+ }
2570
+
2571
+ console.log(\`๐ŸŽฏ Executing \${matching.length} spec(s)\`);
2572
+ return await executeSpecsByIds(matching.map(s => s.id).sort());
2573
+ }
2574
+
2575
+ async function runTest(filter) {
2576
+ if (Array.isArray(filter)) {
2577
+ throw new Error('runTest() only accepts a single spec or RegExp, not an array.');
2578
+ }
2579
+ return runTests(filter);
2580
+ }
2581
+
2582
+ async function runSuite(name) {
2583
+ const suites = getAllSuites();
2584
+ const matching = suites.filter(s =>
2585
+ name instanceof RegExp ? name.test(s.description) : s.description.includes(name)
2586
+ );
2587
+
2588
+ if (!matching.length) {
2589
+ console.warn('No matching suites found for:', name);
2590
+ return [];
2591
+ }
2592
+
2593
+ const allSpecs = matching.flatMap(suite => {
2594
+ const specs = [];
2595
+ const traverse = s => {
2596
+ (s.children || []).forEach(child => {
2597
+ if (isSpec(child)) specs.push(child);
2598
+ if (isSuite(child)) traverse(child);
2599
+ });
2600
+ };
2601
+ traverse(suite);
2602
+ return specs;
2603
+ });
2604
+
2605
+ console.log(\`๐ŸŽฏ Executing \${allSpecs.length} spec(s) from suite\`);
2606
+ return await executeSpecsByIds(allSpecs.map(s => s.id).sort());
2607
+ }
2608
+
2609
+ function listTests() {
2610
+ const specs = getOrderedSpecs(seed, random);
2611
+ console.table(specs.map(s => ({
2612
+ id: s.id,
2613
+ name: s.description
2614
+ })));
2615
+ }
2616
+
2617
+ function setSeed(nextSeed) {
2618
+ const parsed = Number(nextSeed);
2619
+ if (!Number.isFinite(parsed)) {
2620
+ console.warn('Invalid seed (expected a number).');
2621
+ return seed;
2622
+ }
2623
+ random = true;
2624
+ seed = parsed;
2625
+ env.configure({ random, seed });
2626
+ console.log('โœ… Seed updated to:', seed, '| Random enabled:', random);
2627
+ return seed;
2628
+ }
2629
+
2630
+ function resetSeed() {
2631
+ random = false;
2632
+ seed = ${initialSeed};
2633
+ env.configure({ random, seed });
2634
+ console.log('โœ… Seed reset to:', seed, '| Random reset to:', random);
2635
+ return seed;
2636
+ }
2637
+
2638
+ globalThis.runner = {
2639
+ runTests,
2640
+ runTest,
2641
+ runSuite,
2642
+ listTests,
2643
+ setSeed,
2644
+ resetSeed,
2645
+ reload: () => location.reload(),
2646
+ };
2647
+
2648
+ console.log('%cโœ… Jasmine runner ready!', 'color: green; font-weight: bold;');
2649
+ console.log('Usage:');
2650
+ console.log(' await runner.runTest("spec-name") or await runner.runTest(/pattern/)');
2651
+ console.log(' await runner.runTests(["spec1", "spec2"])');
2652
+ console.log(' await runner.runSuite("Suite Name")');
2653
+ console.log(' runner.setSeed(12345) - Enable random order with seed');
2654
+ console.log(' runner.resetSeed() - Back to sequential order');
2655
+ console.log(' runner.listTests() - Show all tests');
2656
+ }
2657
+
2658
+ init().catch(error => {
2659
+ console.error('Failed to initialize runner:', error);
2660
+ });
2661
+ })(window);
2662
+ `;
2663
+ }
2664
+ }
2665
+ class HttpServerManager {
2666
+ constructor(config) {
2667
+ this.config = config;
2668
+ }
2669
+ server = null;
2670
+ async startServer() {
2671
+ const port = this.config.port;
2672
+ const outDir = this.config.outDir;
2673
+ const __filename2 = norm(fileURLToPath(import.meta.url));
2674
+ const __dirname2 = norm(path.dirname(__filename2));
2675
+ const vendorDir = norm(path.join(__dirname2, "../node_modules"));
2676
+ this.server = createServer((req, res) => {
2677
+ let { pathname } = parse(req.url === "/" ? "/index.html" : req.url, true);
2678
+ const filePath = decodeURIComponent(pathname);
2679
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
2680
+ res.setHeader("Pragma", "no-cache");
2681
+ res.setHeader("Expires", "0");
2682
+ if (req.method === "OPTIONS") {
2683
+ res.writeHead(200, {
2684
+ "Access-Control-Allow-Origin": "*",
2685
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
2686
+ "Access-Control-Allow-Headers": "Content-Type"
2687
+ });
2688
+ res.end();
2689
+ return;
2690
+ }
2691
+ let resolvedPath;
2692
+ if (filePath.startsWith("/node_modules/")) {
2693
+ const relativePath = filePath.replace(/^\/node_modules\//, "");
2694
+ resolvedPath = norm(path.join(vendorDir, relativePath));
2695
+ } else {
2696
+ resolvedPath = norm(path.join(outDir, filePath));
2697
+ }
2698
+ resolvedPath = norm(path.normalize(resolvedPath));
2699
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) {
2700
+ const ext = extname(resolvedPath);
2701
+ res.writeHead(200, {
2702
+ "Content-Type": this.getContentType(ext),
2703
+ "Access-Control-Allow-Origin": "*"
2704
+ });
2705
+ res.end(fs.readFileSync(resolvedPath));
2706
+ } else {
2707
+ res.writeHead(404);
2708
+ res.end("Not found");
2709
+ }
2710
+ });
2711
+ return new Promise((resolve, reject) => {
2712
+ this.server.listen(port, () => {
2713
+ logger.println(`๐Ÿš€ Test server running at http://localhost:${port}`);
2714
+ resolve(this.server);
2715
+ });
2716
+ this.server.on("error", (error) => {
2717
+ logger.error(`โŒ Server error: ${error}`);
2718
+ reject(error);
2719
+ });
2720
+ });
2721
+ }
2722
+ getContentType(ext) {
2723
+ const types = {
2724
+ ".html": "text/html",
2725
+ ".js": "application/javascript",
2726
+ ".css": "text/css",
2727
+ ".json": "application/json",
2728
+ ".png": "image/png",
2729
+ ".jpg": "image/jpeg",
2730
+ ".gif": "image/gif",
2731
+ ".svg": "image/svg+xml",
2732
+ ".ico": "image/x-icon"
2733
+ };
2734
+ return types[ext] || "application/octet-stream";
2735
+ }
2736
+ async waitForServerReady(url, timeout = 5e3) {
2737
+ const start = Date.now();
2738
+ const { hostname, port } = new URL(url);
2739
+ while (Date.now() - start < timeout) {
2740
+ try {
2741
+ await new Promise((resolve, reject) => {
2742
+ const req = http.request({
2743
+ hostname,
2744
+ port,
2745
+ path: "/",
2746
+ method: "HEAD",
2747
+ timeout: 1e3
2748
+ }, (res) => {
2749
+ res.resume();
2750
+ resolve();
2751
+ });
2752
+ req.on("error", reject);
2753
+ req.on("timeout", () => {
2754
+ req.destroy();
2755
+ reject(new Error("Timeout"));
2756
+ });
2757
+ req.end();
2758
+ });
2759
+ return;
2760
+ } catch {
2761
+ await new Promise((r) => setTimeout(r, 100));
2762
+ }
2763
+ }
2764
+ throw new Error(`Server not ready at ${url} after ${timeout}ms`);
2765
+ }
2766
+ async cleanup() {
2767
+ if (this.server) {
2768
+ await new Promise((resolve, reject) => {
2769
+ this.server.close((err) => err ? reject(err) : resolve());
2770
+ });
2771
+ this.server = null;
2772
+ }
2773
+ }
2774
+ }
2775
+ class CoverageReportGenerator {
2776
+ reportDir;
2777
+ constructor(reportDir = norm(path__default.join(process.cwd(), "coverage"))) {
2778
+ this.reportDir = reportDir;
2779
+ }
2780
+ saveCoverageToFile(coverage) {
2781
+ try {
2782
+ const fs2 = require2("fs");
2783
+ const path2 = require2("path");
2784
+ const outDir = path2.resolve(process.cwd(), ".nyc_output");
2785
+ const outFile = path2.join(outDir, "out.json");
2786
+ if (!fs2.existsSync(outDir)) fs2.mkdirSync(outDir, { recursive: true });
2787
+ fs2.writeFileSync(outFile, JSON.stringify(coverage, null, 2), "utf8");
2788
+ logger.println(`๐Ÿ“„ Raw coverage saved to ${outFile}`);
2789
+ } catch (err) {
2790
+ logger.error(`โŒ Failed to write coverage file: ${err}`);
2791
+ }
2792
+ }
2793
+ async generate(coverage) {
2794
+ const coverageMap = libCoverage.createCoverageMap(coverage);
2795
+ const remapper = libSourceMaps.createSourceMapStore();
2796
+ const remappedCoverage = await remapper.transformCoverage(coverageMap);
2797
+ const filteredCoverage = libCoverage.createCoverageMap();
2798
+ const filePaths = remappedCoverage.files();
2799
+ for (const filePath of filePaths) {
2800
+ if (!/\.spec\.(ts|tsx|js|jsx|mts|cts|mjs)$/i.test(filePath)) {
2801
+ filteredCoverage.addFileCoverage(remappedCoverage.fileCoverageFor(filePath));
2802
+ }
2803
+ }
2804
+ libReport.createContext({
2805
+ dir: this.reportDir,
2806
+ coverageMap: filteredCoverage
2807
+ });
2808
+ const reporter = libIstanbulApi.createReporter();
2809
+ reporter.dir = this.reportDir;
2810
+ reporter.addAll(["html", "lcov", "text"]);
2811
+ reporter.write(filteredCoverage, true);
2812
+ logger.println(`โœ… Coverage reports generated successfully`);
2813
+ }
2814
+ }
2815
+ class NodeTestRunner {
2816
+ reporter;
2817
+ options;
2818
+ isRunning = false;
2819
+ runnerModule = null;
2820
+ config;
2821
+ constructor(config, options = {}) {
2822
+ this.config = config;
2823
+ this.options = options;
2824
+ this.reporter = options.reporter ?? new ConsoleReporter();
2825
+ }
2826
+ /**
2827
+ * Generate in-process test runner entry file that:
2828
+ * - Bootstraps Jasmine
2829
+ * - Imports compiled spec bundles
2830
+ * - Exposes a stable API: runTests, getOrderedSpecs/Suites, getTestCounts
2831
+ */
2832
+ generateTestRunner() {
2833
+ const outDir = this.config.outDir;
2834
+ if (!fs.existsSync(outDir)) {
2835
+ fs.mkdirSync(outDir, { recursive: true });
2836
+ }
2837
+ const builtFiles = fs.readdirSync(outDir).filter((f) => f.endsWith(".js") && f !== "test-runner.js").sort();
2838
+ if (builtFiles.length === 0) {
2839
+ logger.println("โš ๏ธ No JS files found for test runner generation.");
2840
+ return;
2841
+ }
2842
+ const imports = builtFiles.map((f) => ` await import('./${f}');`).join("\n");
2843
+ const runnerContent = this.generateRunnerTemplate(imports);
2844
+ const testRunnerPath = norm(path.join(outDir, "test-runner.js"));
2845
+ fs.writeFileSync(testRunnerPath, runnerContent);
2846
+ logger.println(
2847
+ `๐Ÿค– Generated in-process test runner: ${norm(path.relative(outDir, testRunnerPath))}`
2848
+ );
2849
+ }
2850
+ /**
2851
+ * Template for the generated ESM runner file.
2852
+ * NOTE: This is emitted as JS, so keep syntax JS-friendly.
2853
+ */
2854
+ generateRunnerTemplate(imports) {
2855
+ return `// Auto-generated in-process Jasmine test runner
2856
+ import { fileURLToPath, pathToFileURL } from 'url';
2857
+ import { dirname, join } from 'path';
2858
+
2859
+ // __dirname / __filename for ESM
2860
+ const __filename = fileURLToPath(import.meta.url);
2861
+ const __dirname = dirname(__filename);
2862
+ const __cwd = process.cwd();
2863
+
2864
+ // Jasmine internals
2865
+ let jasmineInstance = null;
2866
+ let jasmineEnv = null;
2867
+
2868
+ // ---------------------------
2869
+ // Introspection helpers
2870
+ // ---------------------------
2871
+ export function getAllSpecs() {
2872
+ const specs = [];
2873
+ const traverse = (suite) => {
2874
+ suite.children?.forEach((child) => {
2875
+ if (child && typeof child.id === 'string' && !child.children) specs.push(child);
2876
+ if (child?.children) traverse(child);
2877
+ });
2878
+ };
2879
+ traverse(jasmineEnv.topSuite());
2880
+ return specs;
2881
+ }
2882
+
2883
+ export function getAllSuites() {
2884
+ const suites = [];
2885
+ const traverse = (suite) => {
2886
+ suites.push(suite);
2887
+ suite.children?.forEach((child) => {
2888
+ if (child?.children) traverse(child);
2889
+ });
2890
+ };
2891
+ traverse(jasmineEnv.topSuite());
2892
+ return suites;
2893
+ }
2894
+
2895
+ export function getOrderedSpecs(seed, random) {
2896
+ const all = getAllSpecs();
2897
+ if (!random) return all;
2898
+
2899
+ const OrderCtor = jasmineInstance.Order;
2900
+ try {
2901
+ const order = new OrderCtor({ random, seed });
2902
+ return typeof order.sort === "function" ? order.sort(all) : all;
2903
+ } catch {
2904
+ return all;
2905
+ }
2906
+ }
2907
+
2908
+ export function getOrderedSuites(seed, random) {
2909
+ const all = getAllSuites();
2910
+ if (!random) return all;
2911
+
2912
+ const OrderCtor = jasmineInstance.Order;
2913
+ try {
2914
+ const order = new OrderCtor({ random, seed });
2915
+ return typeof order.sort === "function" ? order.sort(all) : all;
2916
+ } catch {
2917
+ return all;
2918
+ }
2919
+ }
2920
+
2921
+ // ---------------------------
2922
+ // Main runTests entrypoint
2923
+ // ---------------------------
2924
+ export async function runTests(reporter) {
2925
+ const envValue = process.env.TS_TEST_RUNNER_SUPPRESS_CONSOLE_LOGS;
2926
+ const shouldSilenceConsole =
2927
+ envValue === '1' || envValue?.toLowerCase() === 'true';
2928
+
2929
+ if (shouldSilenceConsole) {
2930
+ const silentMethods = ['log', 'info', 'debug', 'trace', 'warn', 'table'];
2931
+ for (const method of silentMethods) {
2932
+ if (typeof console[method] === 'function') {
2933
+ console[method] = () => {};
2934
+ }
2935
+ }
2936
+ }
2937
+
2938
+ return new Promise((resolve) => {
2939
+ // Global error handlers
2940
+ process.on('unhandledRejection', (error) => {
2941
+ console.error(\`โŒ Unhandled Rejection: \${error}\`);
2942
+ process.exit(1);
2943
+ });
2944
+
2945
+ process.on('uncaughtException', (error) => {
2946
+ console.error(\`โŒ Uncaught Exception: \${error}\`);
2947
+ process.exit(1);
2948
+ });
2949
+
2950
+ (async function () {
2951
+ try {
2952
+ // Load jasmine-core from testify's own node_modules
2953
+ const jasmineCorePath = join(
2954
+ __cwd,
2955
+ './node_modules/@epikodelabs/testify/node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
2956
+ );
2957
+
2958
+ const jasmineCore = await import(pathToFileURL(jasmineCorePath).href);
2959
+ const jasmineRequire = jasmineCore.default;
2960
+
2961
+ jasmineInstance = jasmineRequire.core(jasmineRequire);
2962
+ jasmineEnv = jasmineInstance.getEnv();
2963
+
2964
+ const utils = {
2965
+ getAllSpecs,
2966
+ getAllSuites,
2967
+ getOrderedSpecs,
2968
+ getOrderedSuites
2969
+ };
2970
+
2971
+ // Expose jasmine globals (describe, it, beforeEach, etc.)
2972
+ Object.assign(globalThis, jasmineRequire.interface(jasmineInstance, jasmineEnv));
2973
+ globalThis.jasmine = { ...globalThis.jasmine, ...jasmineInstance, ...utils };
2974
+
2975
+ // Clean shutdown
2976
+ function onExit(signal) {
2977
+ console.log(\`\\nโš™๏ธ Caught \${signal}. Cleaning up...\`);
2978
+ process.exit(0);
2979
+ }
2980
+ process.on('SIGINT', onExit);
2981
+ process.on('SIGTERM', onExit);
2982
+
2983
+ jasmineEnv.clearReporters();
2984
+ jasmineEnv.addReporter(reporter);
2985
+
2986
+ ${imports}
2987
+
2988
+ // Configure env from template (inlined from ViteJasmineConfig)
2989
+ const random = ${this.config.jasmineConfig?.env?.random ?? false};
2990
+ const stopOnSpecFailure = ${this.config.jasmineConfig?.env?.stopSpecOnExpectationFailure ?? false};
2991
+ const seed = ${this.config.jasmineConfig?.env?.seed} ?? 0;
2992
+
2993
+ jasmineEnv.configure({
2994
+ random,
2995
+ stopOnSpecFailure,
2996
+ seed
2997
+ });
2998
+
2999
+ // Get ordered specs and suites based on configuration
3000
+ const orderedSpecs = getOrderedSpecs(seed, random).map(spec => ({
3001
+ id: spec.id,
3002
+ description: spec.description,
3003
+ fullName: spec.getFullName ? spec.getFullName() : spec.description
3004
+ }));
3005
+
3006
+ const orderedSuites = getOrderedSuites(seed, random).map(suite => ({
3007
+ id: suite.id,
3008
+ description: suite.description,
3009
+ fullName: suite.getFullName ? suite.getFullName() : suite.description
3010
+ }));
3011
+
3012
+ // Execute tests - this will populate spec results
3013
+ await new Promise((resolve) => setTimeout(() => resolve(), 300));
3014
+ reporter.userAgent(undefined, orderedSuites, orderedSpecs);
3015
+ await jasmineEnv.execute();
3016
+
3017
+ const failures = reporter.failureCount || 0;
3018
+ resolve(failures);
3019
+ } catch (error) {
3020
+ console.error(\`โŒ Error during test execution: \${error}\`);
3021
+ console.error(error.stack);
3022
+ resolve(1);
3023
+ }
3024
+ })();
3025
+ });
3026
+ }
3027
+
3028
+ // ---------------------------
3029
+ // CLI entry (backward compat)
3030
+ // ---------------------------
3031
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
3032
+ (async () => {
3033
+ try {
3034
+ const consoleReporterPath = join(__dirname, '../lib/console-reporter.js');
3035
+ const consoleReporterModule = await import(pathToFileURL(consoleReporterPath).href);
3036
+ const ConsoleReporter = consoleReporterModule.ConsoleReporter;
3037
+
3038
+ const failures = await runTests(new ConsoleReporter());
3039
+ process.exit(failures === 0 ? 0 : 1);
3040
+ } catch (error) {
3041
+ console.error(\`โŒ Failed to run tests: \${error}\`);
3042
+ process.exit(1);
3043
+ }
3044
+ })();
3045
+ }
3046
+ `;
3047
+ }
3048
+ /**
3049
+ * Start the test runner in the current (host) process.
3050
+ */
3051
+ async start() {
3052
+ if (this.isRunning) {
3053
+ this.reporter.testsAborted?.("Test process already running");
3054
+ return Promise.reject("Test process already running");
3055
+ }
3056
+ this.isRunning = true;
3057
+ if (this.options.env) {
3058
+ for (const [key, value] of Object.entries(this.options.env)) {
3059
+ if (value == null) delete process.env[key];
3060
+ else process.env[key] = value;
3061
+ }
3062
+ }
3063
+ process.env.NODE_ENV = "test";
3064
+ const shouldSilenceConsole = !!this.options.suppressConsoleLogs;
3065
+ const previousSuppressConsole = process.env.TS_TEST_RUNNER_SUPPRESS_CONSOLE_LOGS;
3066
+ if (shouldSilenceConsole) {
3067
+ process.env.TS_TEST_RUNNER_SUPPRESS_CONSOLE_LOGS = "1";
3068
+ }
3069
+ try {
3070
+ const childFile = path.resolve(
3071
+ this.options.cwd || process.cwd(),
3072
+ this.options.file || path.join(this.config.outDir, "test-runner.js")
3073
+ );
3074
+ logger.println(`๐Ÿš€ Starting test runner in current process...`);
3075
+ const fileUrl = pathToFileURL(childFile).href;
3076
+ this.runnerModule = await import(fileUrl);
3077
+ if (typeof this.runnerModule.runTests === "function") {
3078
+ const failures = await this.runnerModule.runTests(this.reporter);
3079
+ const coverage = globalThis.__coverage__;
3080
+ if (coverage) {
3081
+ const cov = new CoverageReportGenerator();
3082
+ await cov.generate(coverage);
3083
+ }
3084
+ return failures === 0 ? 0 : 1;
3085
+ } else {
3086
+ logger.error("โš ๏ธ Test runner does not export runTests function");
3087
+ return 1;
3088
+ }
3089
+ } catch (error) {
3090
+ this.reporter.jasmineFailed?.(`Test execution error: ${error.message}`);
3091
+ throw error;
3092
+ } finally {
3093
+ if (shouldSilenceConsole) {
3094
+ if (previousSuppressConsole === void 0) {
3095
+ delete process.env.TS_TEST_RUNNER_SUPPRESS_CONSOLE_LOGS;
3096
+ } else {
3097
+ process.env.TS_TEST_RUNNER_SUPPRESS_CONSOLE_LOGS = previousSuppressConsole;
3098
+ }
3099
+ }
3100
+ this.isRunning = false;
3101
+ }
3102
+ }
3103
+ async stop() {
3104
+ this.isRunning = false;
3105
+ }
3106
+ async restart() {
3107
+ await this.stop();
3108
+ setTimeout(() => this.start(), 300);
3109
+ }
3110
+ }
3111
+ class ViteConfigBuilder {
3112
+ constructor(config) {
3113
+ this.config = config;
3114
+ }
3115
+ inputMap = {};
3116
+ static DEFAULT_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
3117
+ "node_modules",
3118
+ "dist",
3119
+ "build",
3120
+ ".git",
3121
+ ".vite",
3122
+ ".cache",
3123
+ ".turbo"
3124
+ ]);
3125
+ /* -------------------------------------------------- */
3126
+ /* Helpers */
3127
+ /* -------------------------------------------------- */
3128
+ preserveRoot() {
3129
+ return this.config.viteBuildOptions?.preserveModulesRoot ?? ".";
3130
+ }
3131
+ normalizeDirs(value, fallback) {
3132
+ if (!value) return [fallback];
3133
+ return Array.isArray(value) ? value : [value];
3134
+ }
3135
+ srcDirs() {
3136
+ return this.normalizeDirs(this.config.srcDirs, "./src");
3137
+ }
3138
+ testDirs() {
3139
+ return this.normalizeDirs(this.config.testDirs, "./tests");
3140
+ }
3141
+ shouldSkipDirectory(dirPath) {
3142
+ const name = path.basename(dirPath);
3143
+ if (ViteConfigBuilder.DEFAULT_EXCLUDED_DIRS.has(name)) {
3144
+ return true;
3145
+ }
3146
+ if (this.config.exclude?.some(
3147
+ (p) => minimatch(dirPath, p, { dot: true })
3148
+ )) {
3149
+ return true;
3150
+ }
3151
+ return false;
3152
+ }
3153
+ isValidSourceFile(file, isTest) {
3154
+ const ext = path.extname(file).toLowerCase();
3155
+ if (![".ts", ".js", ".mjs"].includes(ext)) return false;
3156
+ if (file.endsWith(".d.ts")) return false;
3157
+ const isTestFile = /\.spec\.|\.test\./.test(file);
3158
+ return isTest ? isTestFile : !isTestFile;
3159
+ }
3160
+ /* -------------------------------------------------- */
3161
+ /* Synchronous discovery */
3162
+ /* -------------------------------------------------- */
3163
+ discoverFilesSync() {
3164
+ const all = [];
3165
+ for (const dir of this.srcDirs()) {
3166
+ if (fs.existsSync(dir)) all.push(...this.walk(dir, false));
3167
+ }
3168
+ for (const dir of this.testDirs()) {
3169
+ if (fs.existsSync(dir)) all.push(...this.walk(dir, true));
3170
+ }
3171
+ return [...new Set(all)];
3172
+ }
3173
+ walk(dir, isTest) {
3174
+ const out = [];
3175
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
3176
+ const full = path.join(dir, e.name);
3177
+ if (e.isDirectory()) {
3178
+ if (this.shouldSkipDirectory(full)) continue;
3179
+ out.push(...this.walk(full, isTest));
3180
+ continue;
3181
+ }
3182
+ if (e.isFile() && this.isValidSourceFile(full, isTest)) {
3183
+ out.push(full);
3184
+ }
3185
+ }
3186
+ return out;
3187
+ }
3188
+ /* -------------------------------------------------- */
3189
+ /* Input map (flattened, collision-safe) */
3190
+ /* -------------------------------------------------- */
3191
+ buildInputMap(files) {
3192
+ const map = {};
3193
+ for (const file of files) {
3194
+ if (this.isTypeOnlyModule(file)) {
3195
+ continue;
3196
+ }
3197
+ const rel = path.relative(this.preserveRoot(), file).replace(/\.(ts|js|mjs)$/, "");
3198
+ const flatName = Buffer.from(rel).toString("hex");
3199
+ map[flatName] = norm(file);
3200
+ }
3201
+ return map;
3202
+ }
3203
+ isTypeOnlyModule(filePath) {
3204
+ const ext = path.extname(filePath).toLowerCase();
3205
+ if (![".ts", ".mts", ".cts"].includes(ext)) return false;
3206
+ if (filePath.endsWith(".d.ts")) return true;
3207
+ let content = "";
3208
+ try {
3209
+ content = fs.readFileSync(filePath, "utf8");
3210
+ } catch {
3211
+ return false;
3212
+ }
3213
+ const stripCommentsAndStrings = (input) => {
3214
+ let out = input.replace(/\/\*[\s\S]*?\*\//g, "");
3215
+ out = out.replace(/\/\/.*$/gm, "");
3216
+ out = out.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, "");
3217
+ return out;
3218
+ };
3219
+ const code = stripCommentsAndStrings(content);
3220
+ if (/\bexport\s+\*\s+from\b/.test(code)) return false;
3221
+ if (/\bexport\s+default\b/.test(code)) return false;
3222
+ if (/\bexport\s+(const|let|var|function|class|enum)\b/.test(code)) return false;
3223
+ if (/\bimport\s+(?!type\b)/.test(code)) return false;
3224
+ if (/\b(const|let|var|function|class|enum)\b/.test(code)) return false;
3225
+ for (const match of code.matchAll(/export\s*(?:type\s*)?\{([^}]*)\}/g)) {
3226
+ const specList = match[1].split(",").map((s) => s.trim()).filter(Boolean);
3227
+ for (const spec of specList) {
3228
+ const cleaned = spec.replace(/^type\s+/, "").trim();
3229
+ if (cleaned.length > 0 && !spec.startsWith("type ")) {
3230
+ return false;
3231
+ }
3232
+ }
3233
+ }
3234
+ return true;
3235
+ }
3236
+ /* -------------------------------------------------- */
3237
+ /* Vendor chunk logic */
3238
+ /* -------------------------------------------------- */
3239
+ vendorChunk(id) {
3240
+ if (id.includes("node_modules")) return "vendor";
3241
+ return;
3242
+ }
3243
+ /* -------------------------------------------------- */
3244
+ /* Base config factory */
3245
+ /* -------------------------------------------------- */
3246
+ baseConfig(input, incremental, viteCache) {
3247
+ const onwarn = (warning, warn) => {
3248
+ if (warning.code === "EMPTY_BUNDLE") return;
3249
+ if (warning.code === "CIRCULAR_DEPENDENCY") return;
3250
+ warn(warning);
3251
+ };
3252
+ return {
3253
+ root: process.cwd(),
3254
+ configFile: incremental ? false : void 0,
3255
+ build: {
3256
+ outDir: this.config.outDir,
3257
+ emptyOutDir: !incremental,
3258
+ sourcemap: true,
3259
+ target: "es2022",
3260
+ minify: false,
3261
+ rollupOptions: {
3262
+ input,
3263
+ cache: viteCache,
3264
+ preserveEntrySignatures: incremental ? "allow-extension" : "strict",
3265
+ // ๐Ÿ”ฅ Suppress empty bundle warnings
3266
+ onwarn,
3267
+ output: {
3268
+ format: "es",
3269
+ // ๐Ÿ”ฅ flattened local outputs
3270
+ entryFileNames: "[name].js",
3271
+ // ๐Ÿ”ฅ single vendor bundle
3272
+ chunkFileNames: "vendor.js",
3273
+ manualChunks: (id) => this.vendorChunk(id)
3274
+ }
3275
+ }
3276
+ },
3277
+ resolve: { alias: this.createPathAliases() },
3278
+ esbuild: {
3279
+ target: "es2022",
3280
+ keepNames: false,
3281
+ treeShaking: true
3282
+ // Enable tree shaking for type imports
3283
+ },
3284
+ define: { "process.env.NODE_ENV": '"test"' },
3285
+ logLevel: "warn"
3286
+ };
3287
+ }
3288
+ /* -------------------------------------------------- */
3289
+ /* FULL BUILD */
3290
+ /* -------------------------------------------------- */
3291
+ createViteConfig(entryFiles) {
3292
+ const files = entryFiles && entryFiles.length > 0 ? entryFiles : this.discoverFilesSync();
3293
+ this.inputMap = this.buildInputMap(files);
3294
+ if (!Object.keys(this.inputMap).length) {
3295
+ logger.error("โŒ No files found to build");
3296
+ }
3297
+ return this.mergeUserConfig(this.baseConfig(this.inputMap, false));
3298
+ }
3299
+ /* -------------------------------------------------- */
3300
+ /* INCREMENTAL BUILD */
3301
+ /* -------------------------------------------------- */
3302
+ createViteConfigForFiles(sourceFiles, testFilesOrCache, viteCache) {
3303
+ const testFiles = Array.isArray(testFilesOrCache) ? testFilesOrCache : [];
3304
+ const cache = Array.isArray(testFilesOrCache) ? viteCache : testFilesOrCache;
3305
+ const changedFiles = [...sourceFiles, ...testFiles];
3306
+ const updates = this.buildInputMap(changedFiles);
3307
+ this.inputMap = { ...this.inputMap, ...updates };
3308
+ for (const [k, v] of Object.entries(this.inputMap)) {
3309
+ if (!fs.existsSync(v)) delete this.inputMap[k];
3310
+ }
3311
+ logger.println(
3312
+ `๐Ÿ“ฆ Incremental build: ${Object.keys(this.inputMap).length} files`
3313
+ );
3314
+ return this.mergeUserConfig(
3315
+ this.baseConfig(this.inputMap, true, cache)
3316
+ );
3317
+ }
3318
+ removeFromInputMap(filePath) {
3319
+ const normalized = norm(filePath);
3320
+ for (const [key, value] of Object.entries(this.inputMap)) {
3321
+ if (value === normalized || !fs.existsSync(value)) {
3322
+ delete this.inputMap[key];
3323
+ }
3324
+ }
3325
+ }
3326
+ removeMultipleFromInputMap(filePaths) {
3327
+ const normalizedSet = new Set(filePaths.map(norm));
3328
+ for (const [key, value] of Object.entries(this.inputMap)) {
3329
+ if (normalizedSet.has(value) || !fs.existsSync(value)) {
3330
+ delete this.inputMap[key];
3331
+ }
3332
+ }
3333
+ }
3334
+ /* -------------------------------------------------- */
3335
+ /* Safe user config merge */
3336
+ /* -------------------------------------------------- */
3337
+ mergeUserConfig(base) {
3338
+ const user = this.config.viteConfig;
3339
+ if (!user) return base;
3340
+ return {
3341
+ ...base,
3342
+ ...user,
3343
+ build: {
3344
+ ...base.build,
3345
+ ...user.build,
3346
+ rollupOptions: {
3347
+ ...base.build?.rollupOptions,
3348
+ ...user.build?.rollupOptions
3349
+ }
3350
+ }
3351
+ };
3352
+ }
3353
+ /* -------------------------------------------------- */
3354
+ /* tsconfig aliases */
3355
+ /* -------------------------------------------------- */
3356
+ createPathAliases() {
3357
+ const aliases = {};
3358
+ const cleaner = new JSONCleaner();
3359
+ try {
3360
+ const tsconfigPath = this.config.tsconfig ?? "tsconfig.json";
3361
+ if (!fs.existsSync(tsconfigPath)) return aliases;
3362
+ const tsconfig = cleaner.parse(fs.readFileSync(tsconfigPath, "utf8"));
3363
+ const baseUrl = tsconfig.compilerOptions?.baseUrl ?? ".";
3364
+ const paths = tsconfig.compilerOptions?.paths ?? {};
3365
+ for (const [alias, values] of Object.entries(paths)) {
3366
+ if (!Array.isArray(values) || !values.length) continue;
3367
+ aliases[alias.replace(/\/\*$/, "")] = norm(
3368
+ path.resolve(baseUrl, values[0].replace(/\/\*$/, ""))
3369
+ );
3370
+ }
3371
+ } catch (err) {
3372
+ logger.error(`โš ๏ธ tsconfig parse failed: ${err}`);
3373
+ }
3374
+ return aliases;
3375
+ }
3376
+ }
3377
+ class IstanbulInstrumenter {
3378
+ config;
3379
+ instrumenter;
3380
+ constructor(config) {
3381
+ this.config = config;
3382
+ this.instrumenter = createInstrumenter({
3383
+ coverageVariable: "__coverage__",
3384
+ produceSourceMap: true
3385
+ // generate instrumented map
3386
+ });
3387
+ }
3388
+ async instrument({ filename, source, sourceMap }) {
3389
+ if (!this.config.coverage) return { code: source };
3390
+ if (/\.spec(\.map)?\.js$/i.test(filename)) return { code: source };
3391
+ if (!filename.endsWith(".js")) return { code: source };
3392
+ const instrumentedCode = this.instrumenter.instrumentSync(source, filename, sourceMap);
3393
+ let generatedSourceMap = void 0;
3394
+ if (this.config.coverage && instrumentedCode) ;
3395
+ return {
3396
+ code: instrumentedCode,
3397
+ sourceMap: generatedSourceMap
3398
+ };
3399
+ }
3400
+ /**
3401
+ * Convenience method: read file and instrument it, automatically using existing source map if available
3402
+ */
3403
+ async instrumentFile(filePath) {
3404
+ const source = fs.readFileSync(filePath, "utf-8");
3405
+ const mapFile = filePath + ".map";
3406
+ let sourceMap;
3407
+ if (fs.existsSync(mapFile)) {
3408
+ sourceMap = JSON.parse(fs.readFileSync(mapFile, "utf-8"));
3409
+ }
3410
+ return this.instrument({ filename: filePath, source, sourceMap });
3411
+ }
3412
+ }
3413
+ class WebSocketManager extends EventEmitter {
3414
+ constructor(fileDiscovery, config, server, reporter) {
3415
+ super();
3416
+ this.fileDiscovery = fileDiscovery;
3417
+ this.config = config;
3418
+ this.server = server;
3419
+ this.reporter = reporter;
3420
+ this.createWebSocketServer();
3421
+ }
3422
+ wss = null;
3423
+ wsClients = [];
3424
+ hmrManager = null;
3425
+ hmrEnabled = false;
3426
+ createWebSocketServer() {
3427
+ this.wss = new WebSocketServer({ server: this.server });
3428
+ this.wss.on("connection", async (ws) => {
3429
+ logger.println("๐Ÿ”Œ WebSocket client connected");
3430
+ this.wsClients.push(ws);
3431
+ if (this.hmrEnabled) {
3432
+ const files = await this.fileDiscovery.scanDir(this.config.outDir, "/**/*.js");
3433
+ this.sendToClient(ws, {
3434
+ type: "hmr:connected",
3435
+ specFiles: files.filter((file) => file.endsWith(".spec.js")).map((file) => path__default.basename(file)).sort(),
3436
+ srcFiles: files.filter((file) => !file.endsWith(".spec.js") && file.endsWith(".js")).map((file) => path__default.basename(file)).sort(),
3437
+ enabled: true
3438
+ });
3439
+ }
3440
+ const cleaner = new JSONCleaner();
3441
+ ws.on("message", (data) => {
3442
+ try {
3443
+ const message = cleaner.parse(data.toString());
3444
+ this.handleWebSocketMessage(message);
3445
+ } catch (error) {
3446
+ logger.error(`โŒ Failed to parse WebSocket message: ${error}`);
3447
+ }
3448
+ });
3449
+ ws.on("close", () => {
3450
+ this.wsClients = this.wsClients.filter((client) => client !== ws);
3451
+ });
3452
+ ws.on("error", (error) => {
3453
+ logger.error(`โŒ WebSocket error: ${error}`);
3454
+ this.wsClients = this.wsClients.filter((client) => client !== ws);
3455
+ });
3456
+ });
3457
+ }
3458
+ handleWebSocketMessage(message) {
3459
+ try {
3460
+ switch (message.type) {
3461
+ case "userAgent":
3462
+ this.reporter?.userAgent?.(message.data, message.data.orderedSuites, message.data.orderedSpecs);
3463
+ break;
3464
+ case "jasmineStarted":
3465
+ this.reporter?.jasmineStarted(message);
3466
+ break;
3467
+ case "suiteStarted":
3468
+ this.reporter?.suiteStarted(message);
3469
+ break;
3470
+ case "specStarted":
3471
+ this.reporter?.specStarted(message);
3472
+ break;
3473
+ case "specDone":
3474
+ this.reporter?.specDone(message);
3475
+ break;
3476
+ case "suiteDone":
3477
+ this.reporter?.suiteDone(message);
3478
+ break;
3479
+ case "jasmineDone":
3480
+ this.reporter?.jasmineDone(message);
3481
+ const coverage = message.coverage ? new JSONCleaner().parse(message.coverage) : null;
3482
+ const success = message.overallStatus === "passed" && message.failedSpecsCount === 0;
3483
+ this.emit("testsCompleted", { success, coverage });
3484
+ break;
3485
+ case "hmr:ready":
3486
+ logger.println("๐Ÿ”ฅ Client HMR runtime ready");
3487
+ break;
3488
+ case "hmr:error":
3489
+ logger.error(`โŒ HMR error on client: ${message.error}`);
3490
+ break;
3491
+ default:
3492
+ logger.println(`โš ๏ธ Unknown WebSocket message type: ${message.type}`);
3493
+ }
3494
+ } catch (error) {
3495
+ logger.error(`โŒ Error handling WebSocket message: ${error}`);
3496
+ }
3497
+ }
3498
+ // New method to enable HMR
3499
+ enableHmr(hmrManager) {
3500
+ this.hmrManager = hmrManager;
3501
+ this.hmrEnabled = true;
3502
+ this.hmrManager.on("hmr:update", (update) => {
3503
+ this.broadcast({
3504
+ type: "hmr:update",
3505
+ data: update
3506
+ });
3507
+ });
3508
+ logger.println("๐Ÿ”ฅ HMR enabled on WebSocket server");
3509
+ }
3510
+ broadcast(message) {
3511
+ const data = JSON.stringify(message);
3512
+ this.wsClients.forEach((client) => {
3513
+ if (client.readyState === WebSocket.OPEN) {
3514
+ client.send(data);
3515
+ }
3516
+ });
3517
+ }
3518
+ sendToClient(client, message) {
3519
+ if (client.readyState === WebSocket.OPEN) {
3520
+ client.send(JSON.stringify(message));
3521
+ }
3522
+ }
3523
+ async cleanup() {
3524
+ if (this.hmrManager) {
3525
+ await this.hmrManager.stop();
3526
+ this.hmrManager = null;
3527
+ }
3528
+ if (this.wsClients.length > 0) {
3529
+ for (const client of this.wsClients) {
3530
+ try {
3531
+ if (client.readyState === WebSocket.OPEN) client.close();
3532
+ } catch (err) {
3533
+ logger.error(`โŒ Error closing WebSocket client: ${err}`);
3534
+ }
3535
+ }
3536
+ this.wsClients = [];
3537
+ }
3538
+ if (this.wss) {
3539
+ await new Promise((resolve) => this.wss.close(() => resolve()));
3540
+ this.wss = null;
3541
+ }
3542
+ }
3543
+ }
3544
+ let viteBuild$1 = null;
3545
+ async function getViteBuild() {
3546
+ if (!viteBuild$1) {
3547
+ const vite = await import("vite");
3548
+ viteBuild$1 = vite.build;
3549
+ }
3550
+ return viteBuild$1;
3551
+ }
3552
+ class HmrManager extends EventEmitter {
3553
+ constructor(fileDiscovery, config, viteConfigBuilder, viteCache = null, options) {
3554
+ super();
3555
+ this.fileDiscovery = fileDiscovery;
3556
+ this.config = config;
3557
+ this.viteConfigBuilder = viteConfigBuilder;
3558
+ this.viteCache = viteCache;
3559
+ const srcDir = Array.isArray(config.srcDirs) && config.srcDirs.length > 0 ? config.srcDirs[0] : "./src";
3560
+ const testDir = Array.isArray(config.testDirs) && config.testDirs.length > 0 ? config.testDirs[0] : "./tests";
3561
+ this.primarySrcDir = norm(srcDir);
3562
+ this.primaryTestDir = norm(testDir);
3563
+ this.pathAliases = this.viteConfigBuilder.createPathAliases();
3564
+ if (options?.fileFilter) this.fileFilter = { ...this.fileFilter, ...options.fileFilter };
3565
+ if (options?.rebuildMode) this.rebuildMode = options.rebuildMode;
3566
+ if (options?.sourceChangeStrategy) this.sourceChangeStrategy = options.sourceChangeStrategy;
3567
+ if (options?.criticalSourcePatterns) {
3568
+ this.criticalSourcePatterns = [...this.criticalSourcePatterns, ...options.criticalSourcePatterns];
3569
+ }
3570
+ }
3571
+ watcher = null;
3572
+ isRebuilding = false;
3573
+ rebuildQueue = /* @__PURE__ */ new Set();
3574
+ directChanges = /* @__PURE__ */ new Set();
3575
+ allFiles = [];
3576
+ // โœ… FIX: Add atomic operation queue
3577
+ operationQueue = Promise.resolve();
3578
+ rebuildPromise = null;
3579
+ fileFilter = {
3580
+ include: [],
3581
+ exclude: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/coverage/**"],
3582
+ extensions: [".ts", ".js", ".mjs"]
3583
+ };
3584
+ dependencyGraph = /* @__PURE__ */ new Map();
3585
+ reverseDependencyGraph = /* @__PURE__ */ new Map();
3586
+ pathAliases = {};
3587
+ rebuildMode = "selective";
3588
+ sourceChangeStrategy = "smart";
3589
+ criticalSourcePatterns = [
3590
+ "**/config/**",
3591
+ "**/setup/**",
3592
+ "**/*.config.*",
3593
+ "**/bootstrap.*",
3594
+ "**/main.*",
3595
+ "**/index.*"
3596
+ // root-level index files
3597
+ ];
3598
+ primarySrcDir;
3599
+ primaryTestDir;
3600
+ setFileFilter(filter) {
3601
+ this.fileFilter = { ...this.fileFilter, ...filter };
3602
+ logger.println(`โœ… File filter updated: ${this.fileFilter}`);
3603
+ }
3604
+ setRebuildMode(mode) {
3605
+ this.rebuildMode = mode;
3606
+ logger.println(`โœ… Rebuild mode set to: ${mode}`);
3607
+ }
3608
+ setSourceChangeStrategy(strategy) {
3609
+ this.sourceChangeStrategy = strategy;
3610
+ logger.println(`โœ… Source change strategy set to: ${strategy}`);
3611
+ }
3612
+ matchesFilter(filePath) {
3613
+ const normalizedPath = filePath;
3614
+ if (this.fileFilter.extensions?.length) {
3615
+ const ext = path.extname(normalizedPath);
3616
+ if (!this.fileFilter.extensions.includes(ext)) return false;
3617
+ }
3618
+ if (this.fileFilter.exclude?.length) {
3619
+ if (picomatch.isMatch(normalizedPath, this.fileFilter.exclude)) return false;
3620
+ }
3621
+ if (this.fileFilter.include?.length) {
3622
+ if (!picomatch.isMatch(normalizedPath, this.fileFilter.include)) return false;
3623
+ }
3624
+ return true;
3625
+ }
3626
+ /**
3627
+ * Determines if a file is a test file
3628
+ */
3629
+ isTestFile(filePath) {
3630
+ const normalized = norm(filePath);
3631
+ return normalized.startsWith(this.primaryTestDir);
3632
+ }
3633
+ /**
3634
+ * Determines if a file is a source file
3635
+ */
3636
+ isSourceFile(filePath) {
3637
+ const normalized = norm(filePath);
3638
+ return normalized.startsWith(this.primarySrcDir);
3639
+ }
3640
+ /**
3641
+ * Checks if a source file is critical and requires full reload
3642
+ */
3643
+ isCriticalSourceFile(filePath) {
3644
+ if (!this.isSourceFile(filePath)) return false;
3645
+ const normalized = norm(filePath);
3646
+ return this.criticalSourcePatterns.some(
3647
+ (pattern) => picomatch.isMatch(normalized, pattern)
3648
+ );
3649
+ }
3650
+ /**
3651
+ * Determines the appropriate update strategy based on what changed
3652
+ */
3653
+ determineUpdateStrategy(changedFiles, changeType) {
3654
+ const hasSourceChanges = changedFiles.some((f) => this.isSourceFile(f));
3655
+ const hasTestChanges = changedFiles.some((f) => this.isTestFile(f));
3656
+ const hasCriticalChanges = changedFiles.some((f) => this.isCriticalSourceFile(f));
3657
+ if (!hasSourceChanges && hasTestChanges) {
3658
+ return {
3659
+ type: "test-update",
3660
+ reason: "Test files changed - incremental update"
3661
+ };
3662
+ }
3663
+ if (changeType === "unlink" || changeType === "unlinkDir") {
3664
+ if (hasCriticalChanges) {
3665
+ return {
3666
+ type: "full-reload",
3667
+ reason: "Critical source file/directory removed"
3668
+ };
3669
+ }
3670
+ return {
3671
+ type: "update",
3672
+ reason: "Source file/directory removed - updating dependents"
3673
+ };
3674
+ }
3675
+ if (changeType === "add" || changeType === "addDir") {
3676
+ return {
3677
+ type: "update",
3678
+ reason: "Source file/directory added - building new modules"
3679
+ };
3680
+ }
3681
+ if (hasSourceChanges) {
3682
+ if (this.sourceChangeStrategy === "always-reload") {
3683
+ return {
3684
+ type: "full-reload",
3685
+ reason: "Source changed - always-reload strategy"
3686
+ };
3687
+ }
3688
+ if (this.sourceChangeStrategy === "never-reload") {
3689
+ return {
3690
+ type: "update",
3691
+ reason: "Source changed - never-reload strategy"
3692
+ };
3693
+ }
3694
+ if (hasCriticalChanges) {
3695
+ return {
3696
+ type: "full-reload",
3697
+ reason: "Critical source file changed"
3698
+ };
3699
+ }
3700
+ return {
3701
+ type: "update",
3702
+ reason: "Source changed - incremental update"
3703
+ };
3704
+ }
3705
+ return {
3706
+ type: "update",
3707
+ reason: "General update"
3708
+ };
3709
+ }
3710
+ /**
3711
+ * Rebuilds the dependency graph entry for the given files
3712
+ */
3713
+ async buildDependencyGraph(files) {
3714
+ const existingFiles = files.filter(fs.existsSync);
3715
+ for (const file of existingFiles) {
3716
+ const normalizedFile = norm(file);
3717
+ const oldDeps = this.dependencyGraph.get(normalizedFile);
3718
+ if (oldDeps) {
3719
+ for (const oldDep of oldDeps) {
3720
+ this.reverseDependencyGraph.get(oldDep)?.delete(normalizedFile);
3721
+ }
3722
+ }
3723
+ const newDeps = await this.extractDependencies(file);
3724
+ this.dependencyGraph.set(normalizedFile, newDeps);
3725
+ for (const newDep of newDeps) {
3726
+ if (!this.reverseDependencyGraph.has(newDep)) {
3727
+ this.reverseDependencyGraph.set(newDep, /* @__PURE__ */ new Set());
3728
+ }
3729
+ this.reverseDependencyGraph.get(newDep).add(normalizedFile);
3730
+ }
3731
+ }
3732
+ }
3733
+ async extractDependencies(filePath) {
3734
+ if (!fs.existsSync(filePath)) {
3735
+ return /* @__PURE__ */ new Set();
3736
+ }
3737
+ const deps = /* @__PURE__ */ new Set();
3738
+ try {
3739
+ const content = fs.readFileSync(filePath, "utf-8");
3740
+ const importRegex = /(?:import|export).*?from\s+['"]([^'"]+)['"]/g;
3741
+ const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
3742
+ let match;
3743
+ while ((match = importRegex.exec(content)) !== null) {
3744
+ const resolved = this.resolveImport(filePath, match[1]);
3745
+ if (resolved) deps.add(norm(resolved));
3746
+ }
3747
+ while ((match = requireRegex.exec(content)) !== null) {
3748
+ const resolved = this.resolveImport(filePath, match[1]);
3749
+ if (resolved) deps.add(norm(resolved));
3750
+ }
3751
+ } catch (error) {
3752
+ logger.println(`โš ๏ธ Could not extract dependencies from ${filePath}: ${error.message}`);
3753
+ }
3754
+ return deps;
3755
+ }
3756
+ resolveImport(fromFile, importPath) {
3757
+ if (!fs.existsSync(fromFile)) {
3758
+ return null;
3759
+ }
3760
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
3761
+ const aliasResolved = this.resolvePathAlias(importPath);
3762
+ return aliasResolved || null;
3763
+ }
3764
+ const dir = path.dirname(fromFile);
3765
+ let resolved = path.resolve(dir, importPath);
3766
+ const extensions = [...this.fileFilter.extensions, ""];
3767
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
3768
+ for (const ext of extensions) {
3769
+ const withExt = resolved + ext;
3770
+ if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) return withExt;
3771
+ const indexFile = path.join(resolved, `index${ext}`);
3772
+ if (fs.existsSync(indexFile) && fs.statSync(indexFile).isFile()) return indexFile;
3773
+ }
3774
+ return null;
3775
+ }
3776
+ resolvePathAlias(importPath) {
3777
+ const extensions = [...this.fileFilter.extensions, ""];
3778
+ for (const [alias, aliasPath] of Object.entries(this.pathAliases)) {
3779
+ if (importPath === alias || importPath.startsWith(alias.replace(/\/\*$/, "") + "/")) {
3780
+ const relativePart = importPath.slice(alias.replace(/\/\*$/, "").length);
3781
+ const resolvedBase = norm(path.join(aliasPath.replace(/\/\*$/, ""), relativePart));
3782
+ for (const ext of extensions) {
3783
+ const withExt = resolvedBase + ext;
3784
+ if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) return withExt;
3785
+ const indexFile = path.join(resolvedBase, `index${ext}`);
3786
+ if (fs.existsSync(indexFile) && fs.statSync(indexFile).isFile()) return indexFile;
3787
+ }
3788
+ }
3789
+ }
3790
+ return null;
3791
+ }
3792
+ getFilesToRebuild(changedFile) {
3793
+ const filesToRebuild = /* @__PURE__ */ new Set();
3794
+ if (this.rebuildMode === "all") {
3795
+ this.allFiles.filter(fs.existsSync).forEach((f) => filesToRebuild.add(f));
3796
+ return filesToRebuild;
3797
+ }
3798
+ const queue = [norm(changedFile)];
3799
+ const visited = /* @__PURE__ */ new Set();
3800
+ while (queue.length) {
3801
+ const current = queue.shift();
3802
+ if (visited.has(current)) continue;
3803
+ visited.add(current);
3804
+ if (fs.existsSync(current)) {
3805
+ filesToRebuild.add(current);
3806
+ }
3807
+ const dependents = this.reverseDependencyGraph.get(current);
3808
+ if (dependents) {
3809
+ dependents.forEach((d) => {
3810
+ if (fs.existsSync(d)) {
3811
+ queue.push(d);
3812
+ }
3813
+ });
3814
+ }
3815
+ }
3816
+ return filesToRebuild;
3817
+ }
3818
+ /**
3819
+ * Gets all test files affected by a source change
3820
+ */
3821
+ getAffectedTests(sourceFile) {
3822
+ const allDependents = this.getFilesToRebuild(sourceFile);
3823
+ return Array.from(allDependents).filter((f) => this.isTestFile(f) && fs.existsSync(f));
3824
+ }
3825
+ async start() {
3826
+ const watchDirs = [...this.config.srcDirs || [], ...this.config.testDirs || []].filter(Boolean);
3827
+ const watchTargets = watchDirs.length > 0 ? watchDirs : [this.primarySrcDir, this.primaryTestDir];
3828
+ this.initializeTrackedFiles(watchTargets);
3829
+ this.watcher = watch(watchTargets, {
3830
+ ignored: /(^|[\/\\])\../,
3831
+ persistent: true,
3832
+ ignoreInitial: true
3833
+ });
3834
+ this.watcher.on("change", (filePath) => {
3835
+ filePath = norm(filePath);
3836
+ if (this.matchesFilter(filePath)) this.queueRebuild(filePath, "change");
3837
+ });
3838
+ this.watcher.on("add", (filePath) => {
3839
+ filePath = norm(filePath);
3840
+ if (this.matchesFilter(filePath)) this.handleFileAdd(filePath);
3841
+ });
3842
+ this.watcher.on("unlink", (filePath) => {
3843
+ filePath = norm(filePath);
3844
+ if (this.matchesFilter(filePath)) this.handleFileRemove(filePath);
3845
+ });
3846
+ this.watcher.on("addDir", (dirPath) => this.handleDirectoryAdd(norm(dirPath)));
3847
+ this.watcher.on("unlinkDir", (dirPath) => this.handleDirectoryRemove(norm(dirPath)));
3848
+ this.watcher.on("ready", async () => {
3849
+ logger.println(`โœ… HMR watching ${this.allFiles.length} files (mode: ${this.rebuildMode}, strategy: ${this.sourceChangeStrategy})`);
3850
+ this.emit("hmr:ready");
3851
+ });
3852
+ }
3853
+ initializeTrackedFiles(watchTargets) {
3854
+ const defaultExtensions = this.fileFilter.extensions.join(",");
3855
+ for (const target of watchTargets) {
3856
+ const normalizedTarget = norm(target);
3857
+ if (!fs.existsSync(normalizedTarget)) continue;
3858
+ const pattern = norm(path.join(normalizedTarget, `**/*{${defaultExtensions}}`));
3859
+ const files = glob.sync(pattern, { absolute: true, ignore: ["**/node_modules/**"] }).filter((f) => this.matchesFilter(norm(f)));
3860
+ for (const file of files) {
3861
+ const normalized = norm(file);
3862
+ if (!this.allFiles.includes(normalized)) {
3863
+ this.allFiles.push(normalized);
3864
+ }
3865
+ }
3866
+ }
3867
+ }
3868
+ async handleFileAdd(filePath) {
3869
+ this.operationQueue = this.operationQueue.then(async () => {
3870
+ filePath = norm(filePath);
3871
+ if (!this.allFiles.includes(filePath)) {
3872
+ this.allFiles.push(filePath);
3873
+ const fileType = this.isTestFile(filePath) ? "test" : this.isSourceFile(filePath) ? "source" : "unknown";
3874
+ const output = norm(this.isTestFile(filePath) ? path.relative(this.primaryTestDir, filePath) : path.relative(this.primarySrcDir, filePath));
3875
+ logger.println(`โž• ${capitalize(fileType)} file added: ${output}`);
3876
+ this.queueRebuild(filePath, "add");
3877
+ }
3878
+ }).catch((error) => {
3879
+ logger.error(`โŒ Error in handleFileAdd: ${error}`);
3880
+ });
3881
+ await this.operationQueue;
3882
+ }
3883
+ async handleFileRemove(filePath) {
3884
+ this.operationQueue = this.operationQueue.then(async () => {
3885
+ filePath = norm(filePath);
3886
+ this.viteConfigBuilder.removeFromInputMap(filePath);
3887
+ this.rebuildQueue.delete(filePath);
3888
+ this.directChanges.delete(filePath);
3889
+ const affectedFiles = /* @__PURE__ */ new Set();
3890
+ const dependents = this.reverseDependencyGraph.get(filePath);
3891
+ dependents?.forEach((d) => {
3892
+ if (fs.existsSync(d)) {
3893
+ affectedFiles.add(d);
3894
+ }
3895
+ });
3896
+ this.allFiles = this.allFiles.filter((f) => f !== filePath);
3897
+ this.dependencyGraph.delete(filePath);
3898
+ this.reverseDependencyGraph.delete(filePath);
3899
+ for (const deps of this.dependencyGraph.values()) deps.delete(filePath);
3900
+ for (const dep of this.reverseDependencyGraph.values()) dep.delete(filePath);
3901
+ const fileType = this.isTestFile(filePath) ? "test" : this.isSourceFile(filePath) ? "source" : "unknown";
3902
+ let output = norm(this.isTestFile(filePath) ? path.relative(this.primaryTestDir, filePath) : path.relative(this.primarySrcDir, filePath));
3903
+ logger.println(`โž– ${capitalize(fileType)} file removed: ${output}`);
3904
+ const strategy = this.determineUpdateStrategy([filePath], "unlink");
3905
+ output = norm(path.join(this.config.outDir, this.fileDiscovery.getOutputName(filePath)));
3906
+ if (fs.existsSync(output)) fs.rmSync(output);
3907
+ const map = output.replace(/\.js$/, ".js.map");
3908
+ if (fs.existsSync(map)) fs.rmSync(map);
3909
+ this.emit("hmr:update", {
3910
+ type: strategy.type,
3911
+ path: this.fileDiscovery.getOutputName(filePath),
3912
+ timestamp: Date.now(),
3913
+ affectedTests: this.isSourceFile(filePath) ? Array.from(affectedFiles).filter((f) => this.isTestFile(f)) : void 0,
3914
+ reason: strategy.reason
3915
+ });
3916
+ if (this.rebuildMode === "selective" && affectedFiles.size > 0) {
3917
+ affectedFiles.forEach((f) => this.queueRebuild(f, "change"));
3918
+ }
3919
+ }).catch((error) => {
3920
+ logger.error(`โŒ Error in handleFileRemove: ${error}`);
3921
+ });
3922
+ await this.operationQueue;
3923
+ }
3924
+ async handleDirectoryAdd(dirPath) {
3925
+ this.operationQueue = this.operationQueue.then(async () => {
3926
+ dirPath = norm(dirPath);
3927
+ const dirType = dirPath.startsWith(this.primaryTestDir) ? "test" : "source";
3928
+ const output = norm(dirPath.startsWith(this.primaryTestDir) ? path.relative(this.primaryTestDir, dirPath) : path.relative(this.primarySrcDir, dirPath));
3929
+ logger.println(`๐Ÿ“ ${capitalize(dirType)} directory added: ${output}`);
3930
+ const defaultExtensions = this.fileFilter.extensions.join(",");
3931
+ const pattern = norm(path.join(dirPath, `**/*{${defaultExtensions}}`));
3932
+ const newFiles = glob.sync(pattern, { absolute: true, ignore: ["**/node_modules/**"] }).filter((f) => this.matchesFilter(norm(f)));
3933
+ const filesToProcess = [];
3934
+ for (const file of newFiles) {
3935
+ const normalized = norm(file);
3936
+ if (!this.allFiles.includes(normalized)) {
3937
+ this.allFiles.push(normalized);
3938
+ filesToProcess.push(normalized);
3939
+ }
3940
+ }
3941
+ if (filesToProcess.length) {
3942
+ logger.println(`๐Ÿ“ฆ Found ${filesToProcess.length} ${dirType} files in new directory`);
3943
+ const strategy = this.determineUpdateStrategy(filesToProcess, "addDir");
3944
+ this.emit("hmr:update", {
3945
+ type: strategy.type,
3946
+ path: output,
3947
+ timestamp: Date.now(),
3948
+ reason: strategy.reason
3949
+ });
3950
+ filesToProcess.forEach((f) => this.queueRebuild(f, "add"));
3951
+ }
3952
+ }).catch((error) => {
3953
+ logger.error(`โŒ Error in handleDirectoryAdd: ${error}`);
3954
+ });
3955
+ await this.operationQueue;
3956
+ }
3957
+ async handleDirectoryRemove(dirPath) {
3958
+ this.operationQueue = this.operationQueue.then(async () => {
3959
+ dirPath = norm(dirPath);
3960
+ const dirType = dirPath.startsWith(this.primaryTestDir) ? "test" : "source";
3961
+ const output = norm(dirPath.startsWith(this.primaryTestDir) ? path.relative(this.primaryTestDir, dirPath) : path.relative(this.primarySrcDir, dirPath));
3962
+ logger.println(`๐Ÿ“ ${capitalize(dirType)} directory removed: ${output}`);
3963
+ const removedFiles = this.allFiles.filter((f) => f.startsWith(dirPath + path.sep) || f === dirPath);
3964
+ const affectedFiles = /* @__PURE__ */ new Set();
3965
+ for (const file of removedFiles) {
3966
+ this.rebuildQueue.delete(file);
3967
+ this.directChanges.delete(file);
3968
+ const dependents = this.reverseDependencyGraph.get(file);
3969
+ dependents?.forEach((d) => {
3970
+ if (fs.existsSync(d)) {
3971
+ affectedFiles.add(d);
3972
+ }
3973
+ });
3974
+ this.dependencyGraph.delete(file);
3975
+ this.reverseDependencyGraph.delete(file);
3976
+ for (const deps of this.dependencyGraph.values()) deps.delete(file);
3977
+ for (const dep of this.reverseDependencyGraph.values()) dep.delete(file);
3978
+ }
3979
+ this.allFiles = this.allFiles.filter((f) => !removedFiles.includes(f));
3980
+ const strategy = this.determineUpdateStrategy(removedFiles, "unlinkDir");
3981
+ this.viteConfigBuilder.removeMultipleFromInputMap(removedFiles);
3982
+ this.emit("hmr:update", {
3983
+ type: strategy.type,
3984
+ path: output,
3985
+ timestamp: Date.now(),
3986
+ affectedTests: Array.from(affectedFiles).filter((f) => this.isTestFile(f)),
3987
+ reason: strategy.reason
3988
+ });
3989
+ if (this.rebuildMode === "selective" && affectedFiles.size > 0) {
3990
+ affectedFiles.forEach((f) => this.queueRebuild(f, "change"));
3991
+ }
3992
+ }).catch((error) => {
3993
+ logger.error(`โŒ Error in handleDirectoryRemove: ${error}`);
3994
+ });
3995
+ await this.operationQueue;
3996
+ }
3997
+ async queueRebuild(filePath, changeType = "change") {
3998
+ const normalized = norm(filePath);
3999
+ if (changeType !== "unlink" && !fs.existsSync(normalized)) {
4000
+ logger.println(`โš ๏ธ Skipping rebuild for non-existent file: ${normalized}`);
4001
+ return;
4002
+ }
4003
+ this.directChanges.add(normalized);
4004
+ this.rebuildQueue.add(normalized);
4005
+ if (this.rebuildPromise) {
4006
+ await this.rebuildPromise;
4007
+ }
4008
+ if (!this.isRebuilding) {
4009
+ this.isRebuilding = true;
4010
+ this.rebuildPromise = this.rebuildAll().catch((error) => {
4011
+ logger.error(`โŒ Rebuild failed: ${error}`);
4012
+ this.emit(`hmr:error ${error}`);
4013
+ }).finally(() => {
4014
+ this.isRebuilding = false;
4015
+ this.rebuildPromise = null;
4016
+ });
4017
+ }
4018
+ await this.rebuildPromise;
4019
+ }
4020
+ async rebuildAll() {
4021
+ try {
4022
+ while (this.rebuildQueue.size > 0) {
4023
+ const startTime = Date.now();
4024
+ const changedFiles = Array.from(this.rebuildQueue).filter((file) => {
4025
+ if (!fs.existsSync(file)) {
4026
+ logger.println(`โš ๏ธ Skipping deleted file from rebuild queue: ${file}`);
4027
+ return false;
4028
+ }
4029
+ return true;
4030
+ });
4031
+ this.rebuildQueue.clear();
4032
+ const directChangedFiles = Array.from(this.directChanges).filter((file) => {
4033
+ if (!fs.existsSync(file)) {
4034
+ logger.println(`โš ๏ธ Skipping deleted file from direct changes: ${file}`);
4035
+ return false;
4036
+ }
4037
+ return true;
4038
+ });
4039
+ this.directChanges.clear();
4040
+ if (changedFiles.length === 0) {
4041
+ logger.println("โš ๏ธ All queued files were deleted, skipping rebuild");
4042
+ continue;
4043
+ }
4044
+ const filesToRebuild = /* @__PURE__ */ new Set();
4045
+ for (const file of changedFiles) {
4046
+ const deps = this.getFilesToRebuild(file);
4047
+ deps.forEach((f) => {
4048
+ if (fs.existsSync(f)) {
4049
+ filesToRebuild.add(f);
4050
+ }
4051
+ });
4052
+ }
4053
+ const rebuiltFiles = Array.from(filesToRebuild);
4054
+ if (rebuiltFiles.length === 0) {
4055
+ logger.println("โš ๏ธ No valid files to rebuild after filtering");
4056
+ continue;
4057
+ }
4058
+ const validSourceFiles = rebuiltFiles.filter((f) => this.isSourceFile(f) && fs.existsSync(f));
4059
+ const validTestFiles = rebuiltFiles.filter((f) => this.isTestFile(f) && fs.existsSync(f));
4060
+ logger.println(
4061
+ `๐Ÿ“ฆ Changed: ${directChangedFiles.length} files โ†’ Rebuilding: ${rebuiltFiles.length} files (${validSourceFiles.length} source, ${validTestFiles.length} test)`
4062
+ );
4063
+ if (validSourceFiles.length === 0 && validTestFiles.length === 0) {
4064
+ logger.println("โš ๏ธ No valid source or test files to build after filtering");
4065
+ continue;
4066
+ }
4067
+ await this.buildDependencyGraph(rebuiltFiles);
4068
+ const viteConfig = this.viteConfigBuilder.createViteConfigForFiles(
4069
+ [...validSourceFiles, ...validTestFiles],
4070
+ this.viteCache
4071
+ );
4072
+ const build = await getViteBuild();
4073
+ const startBuildTime = Date.now();
4074
+ try {
4075
+ const result = await build(viteConfig);
4076
+ this.viteCache = result;
4077
+ } catch (buildError) {
4078
+ logger.error(`โŒ Vite build failed: ${buildError}`);
4079
+ if (buildError.code === "UNRESOLVED_ENTRY") {
4080
+ logger.println("๐Ÿ”„ Retrying build with filtered entry points...");
4081
+ const finalSourceFiles = validSourceFiles.filter(fs.existsSync);
4082
+ const finalTestFiles = validTestFiles.filter(fs.existsSync);
4083
+ if (finalSourceFiles.length === 0 && finalTestFiles.length === 0) {
4084
+ logger.println("โš ๏ธ All entry points were deleted, skipping build");
4085
+ continue;
4086
+ }
4087
+ const retryConfig = this.viteConfigBuilder.createViteConfigForFiles(
4088
+ [...finalSourceFiles, ...finalTestFiles],
4089
+ this.viteCache
4090
+ );
4091
+ const result = await build(retryConfig);
4092
+ this.viteCache = result;
4093
+ } else {
4094
+ throw buildError;
4095
+ }
4096
+ }
4097
+ for (const file of rebuiltFiles) {
4098
+ const relative = this.fileDiscovery.getOutputName(file);
4099
+ const outputPath = path.join(this.config.outDir, relative);
4100
+ if (fs.existsSync(outputPath)) {
4101
+ const content = fs.readFileSync(outputPath, "utf-8");
4102
+ const strategy = this.determineUpdateStrategy(directChangedFiles, "change");
4103
+ const affectedTests = directChangedFiles.filter((f) => this.isSourceFile(f)).flatMap((f) => this.getAffectedTests(f));
4104
+ this.emit("hmr:update", {
4105
+ type: strategy.type,
4106
+ path: relative,
4107
+ timestamp: Date.now(),
4108
+ content,
4109
+ affectedTests: affectedTests.length > 0 ? affectedTests : void 0,
4110
+ reason: strategy.reason
4111
+ });
4112
+ }
4113
+ }
4114
+ logger.println(`๐Ÿ“ฆ Vite rebuild completed in ${Date.now() - startBuildTime}ms`);
4115
+ const duration = Date.now() - startTime;
4116
+ const sourceChanges = directChangedFiles.filter((f) => this.isSourceFile(f));
4117
+ const testChanges = directChangedFiles.filter((f) => this.isTestFile(f));
4118
+ const updateType = sourceChanges.length > 0 ? "source-change" : testChanges.length > 0 ? "test-only" : "full";
4119
+ this.emit("hmr:rebuild", {
4120
+ changedFiles: directChangedFiles,
4121
+ rebuiltFiles,
4122
+ duration,
4123
+ timestamp: Date.now(),
4124
+ updateType
4125
+ });
4126
+ logger.println(`โœ… Rebuild complete (${updateType}): ${rebuiltFiles.length} files in ${duration}ms`);
4127
+ }
4128
+ } catch (error) {
4129
+ logger.error(`โŒ Rebuild failed: ${error}`);
4130
+ this.emit(`hmr:error ${error}`);
4131
+ throw error;
4132
+ }
4133
+ }
4134
+ getDependencyInfo(filePath) {
4135
+ const normalized = norm(filePath);
4136
+ return {
4137
+ dependencies: Array.from(this.dependencyGraph.get(normalized) || []),
4138
+ dependents: Array.from(this.reverseDependencyGraph.get(normalized) || []),
4139
+ isTest: this.isTestFile(normalized),
4140
+ isSource: this.isSourceFile(normalized),
4141
+ isCritical: this.isCriticalSourceFile(normalized),
4142
+ affectedTests: this.isSourceFile(normalized) ? this.getAffectedTests(normalized) : []
4143
+ };
4144
+ }
4145
+ getStats() {
4146
+ const sourceFiles = this.allFiles.filter((f) => this.isSourceFile(f));
4147
+ const testFiles = this.allFiles.filter((f) => this.isTestFile(f));
4148
+ const criticalFiles = sourceFiles.filter((f) => this.isCriticalSourceFile(f));
4149
+ return {
4150
+ totalFiles: this.allFiles.length,
4151
+ sourceFiles: sourceFiles.length,
4152
+ testFiles: testFiles.length,
4153
+ criticalSourceFiles: criticalFiles.length,
4154
+ trackedDependencies: this.dependencyGraph.size,
4155
+ rebuildMode: this.rebuildMode,
4156
+ sourceChangeStrategy: this.sourceChangeStrategy,
4157
+ fileFilter: this.fileFilter,
4158
+ pathAliases: this.pathAliases,
4159
+ criticalPatterns: this.criticalSourcePatterns
4160
+ };
4161
+ }
4162
+ async stop() {
4163
+ if (this.watcher) {
4164
+ await this.watcher.close();
4165
+ this.watcher = null;
4166
+ this.dependencyGraph.clear();
4167
+ this.reverseDependencyGraph.clear();
4168
+ logger.println("โœ… HMR watcher stopped");
4169
+ }
4170
+ }
4171
+ }
4172
+ const { build: viteBuild } = await import("vite");
4173
+ class ViteJasmineRunner extends EventEmitter {
4174
+ viteCache = null;
4175
+ config;
4176
+ fileDiscovery;
4177
+ viteConfigBuilder;
4178
+ htmlGenerator;
4179
+ browserManager;
4180
+ httpServerManager;
4181
+ nodeTestRunner;
4182
+ webSocketManager = null;
4183
+ consoleReporter;
4184
+ instrumenter;
4185
+ hmrManager = null;
4186
+ completePromise = new Promise((resolve, reject) => {
4187
+ this.completePromiseResolve = resolve;
4188
+ });
4189
+ completePromiseResolve = null;
4190
+ primarySrcDir;
4191
+ primaryTestDir;
4192
+ shouldPreserve() {
4193
+ return !!this.config.preserveOutputs;
4194
+ }
4195
+ constructor(config) {
4196
+ super();
4197
+ const cwd = norm(process.cwd());
4198
+ const normalizedSrcDirs = (Array.isArray(config.srcDirs) ? config.srcDirs : [config.srcDirs ?? "./src"]).filter(Boolean).map(norm);
4199
+ const normalizedTestDirs = (Array.isArray(config.testDirs) ? config.testDirs : [config.testDirs ?? "./tests"]).filter(Boolean).map(norm);
4200
+ this.primarySrcDir = normalizedSrcDirs[0] ?? cwd;
4201
+ this.primaryTestDir = normalizedTestDirs[0] ?? cwd;
4202
+ this.config = {
4203
+ ...config,
4204
+ browser: config.browser ?? "node",
4205
+ port: config.port ?? 8888,
4206
+ headless: config.headless ?? true,
4207
+ watch: config.watch ?? false,
4208
+ srcDirs: normalizedSrcDirs,
4209
+ testDirs: normalizedTestDirs,
4210
+ outDir: norm(config.outDir) ?? norm(path.join(cwd, "dist/.vite-jasmine-build/"))
4211
+ };
4212
+ this.fileDiscovery = new FileDiscoveryService(this.config);
4213
+ this.viteConfigBuilder = new ViteConfigBuilder(this.config);
4214
+ this.htmlGenerator = new HtmlGenerator(this.fileDiscovery, this.config);
4215
+ this.browserManager = new BrowserManager(this.config);
4216
+ this.httpServerManager = new HttpServerManager(this.config);
4217
+ this.instrumenter = new IstanbulInstrumenter(this.config);
4218
+ this.consoleReporter = new ConsoleReporter();
4219
+ this.nodeTestRunner = new NodeTestRunner(this.config, {
4220
+ reporter: this.consoleReporter,
4221
+ cwd: this.config.outDir,
4222
+ file: "test-runner.js",
4223
+ coverage: this.config.coverage,
4224
+ suppressConsoleLogs: this.config.suppressConsoleLogs
4225
+ });
4226
+ }
4227
+ async preprocess() {
4228
+ try {
4229
+ const { srcFiles, specFiles } = await this.fileDiscovery.discoverSources();
4230
+ if (specFiles.length === 0) {
4231
+ throw new Error("No test files found");
4232
+ }
4233
+ const entryFiles = [...srcFiles, ...specFiles];
4234
+ const viteConfig = this.viteConfigBuilder.createViteConfig(entryFiles);
4235
+ const input = {};
4236
+ const entryKeyFromOutput = (file) => this.fileDiscovery.getOutputName(file).replace(/\.js$/, "");
4237
+ for (const file of entryFiles) {
4238
+ input[entryKeyFromOutput(file)] = file;
4239
+ }
4240
+ if (!fs.existsSync(this.config.outDir)) {
4241
+ fs.mkdirSync(this.config.outDir, { recursive: true });
4242
+ }
4243
+ viteConfig.build.rollupOptions.input = input;
4244
+ logger.println(`๐Ÿ“ฆ Building ${Object.keys(input).length} files...`);
4245
+ this.viteCache = await viteBuild(viteConfig);
4246
+ const jsFiles = glob.sync(path.join(this.config.outDir, "**/*.js").replace(/\\/g, "/")).filter((f) => !/\.spec\.js$/i.test(f));
4247
+ for (const jsFile of jsFiles) {
4248
+ const result = await this.instrumenter.instrumentFile(jsFile);
4249
+ const outFile = path.join(this.config.outDir, path.relative(this.config.outDir, jsFile));
4250
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
4251
+ fs.writeFileSync(outFile, result.code, "utf-8");
4252
+ if (result.sourceMap) {
4253
+ const mapFile = outFile + ".map";
4254
+ fs.writeFileSync(mapFile, JSON.stringify(result.sourceMap, null, 2), "utf-8");
4255
+ }
4256
+ }
4257
+ const htmlPath = path.join(this.config.outDir, "index.html");
4258
+ const preserveHtml = this.shouldPreserve() && fs.existsSync(htmlPath);
4259
+ if (!(this.config.headless && this.config.browser === "node") && !preserveHtml) {
4260
+ if (this.config.watch) {
4261
+ await this.htmlGenerator.generateHtmlFileWithHmr();
4262
+ } else {
4263
+ await this.htmlGenerator.generateHtmlFile();
4264
+ }
4265
+ } else if (preserveHtml) {
4266
+ logger.println("โ„น๏ธ Preserving existing index.html (no regeneration).");
4267
+ }
4268
+ const runnerPath = path.join(this.config.outDir, "test-runner.js");
4269
+ const preserveRunner = this.shouldPreserve() && fs.existsSync(runnerPath);
4270
+ if (this.config.headless && this.config.browser === "node" && !preserveRunner) {
4271
+ this.nodeTestRunner.generateTestRunner();
4272
+ } else if (this.config.headless && this.config.browser === "node" && preserveRunner) {
4273
+ logger.println("โ„น๏ธ Preserving existing test-runner.js (no regeneration).");
4274
+ }
4275
+ } catch (error) {
4276
+ logger.error(`โŒ Preprocessing failed: ${error}`);
4277
+ throw error;
4278
+ }
4279
+ }
4280
+ async cleanup() {
4281
+ if (this.hmrManager) {
4282
+ await this.hmrManager.stop();
4283
+ this.hmrManager = null;
4284
+ }
4285
+ if (this.webSocketManager) {
4286
+ await this.webSocketManager.cleanup();
4287
+ this.webSocketManager = null;
4288
+ }
4289
+ await this.httpServerManager.cleanup();
4290
+ }
4291
+ async start() {
4292
+ if (this.config.watch) {
4293
+ return this.watch();
4294
+ }
4295
+ logger.println(
4296
+ `๐Ÿš€ Starting Jasmine Test ${this.config.headless ? "Runner (Headless)" : "Server"}...`
4297
+ );
4298
+ try {
4299
+ await this.preprocess();
4300
+ } catch (error) {
4301
+ logger.error(`โŒ Build failed: ${error}`);
4302
+ process.exit(1);
4303
+ }
4304
+ if (this.config.headless && this.config.browser !== "node") {
4305
+ await this.runHeadlessBrowserMode();
4306
+ } else if (this.config.headless && this.config.browser === "node") {
4307
+ await this.runHeadlessNodeMode();
4308
+ } else if (!this.config.headless && this.config.browser === "node") {
4309
+ logger.error(`โŒ Invalid configuration: Node.js runner cannot run in headed mode.`);
4310
+ process.exit(1);
4311
+ } else {
4312
+ await this.runHeadedBrowserMode();
4313
+ }
4314
+ }
4315
+ async watch() {
4316
+ if (this.config.headless || this.config.browser === "node") {
4317
+ logger.error("โŒ --watch mode is only supported in headed browser environments.");
4318
+ process.exit(1);
4319
+ }
4320
+ this.config.watch = true;
4321
+ logger.println("๐Ÿ‘€ Starting Jasmine Tests Runner in Watch Mode...");
4322
+ await this.preprocess();
4323
+ await this.runWatchMode();
4324
+ }
4325
+ async runWatchMode() {
4326
+ logger.println("๐Ÿ”ฅ Starting HMR file watcher...");
4327
+ const server = await this.httpServerManager.startServer();
4328
+ this.webSocketManager = new WebSocketManager(this.fileDiscovery, this.config, server, this.consoleReporter);
4329
+ this.hmrManager = new HmrManager(this.fileDiscovery, this.config, this.viteConfigBuilder, this.viteCache);
4330
+ this.webSocketManager.enableHmr(this.hmrManager);
4331
+ await this.hmrManager.start();
4332
+ logger.println("๐Ÿ“ก WebSocket server ready");
4333
+ logger.println("๐Ÿ‘Œ Press Ctrl+C to stop the server");
4334
+ let shuttingDown = false;
4335
+ const onBrowserClose = async () => {
4336
+ if (shuttingDown) return;
4337
+ logger.println("๐Ÿ”„ Browser window closed");
4338
+ await this.cleanup();
4339
+ process.exit(0);
4340
+ };
4341
+ await this.browserManager.openBrowser(this.config.port, onBrowserClose, { exitOnClose: false });
4342
+ process.once("SIGINT", async () => {
4343
+ if (shuttingDown) return;
4344
+ shuttingDown = true;
4345
+ logger.println("๐Ÿ›‘ Stopping HMR server...");
4346
+ await this.browserManager.closeBrowser();
4347
+ logger.println("๐Ÿ”„ Browser window closed");
4348
+ await this.cleanup();
4349
+ process.exit(0);
4350
+ });
4351
+ process.once("SIGTERM", async () => {
4352
+ if (shuttingDown) return;
4353
+ shuttingDown = true;
4354
+ logger.println("๐Ÿ›‘ Received SIGTERM, stopping HMR server...");
4355
+ await this.browserManager.closeBrowser();
4356
+ logger.println("๐Ÿ”„ Browser window closed");
4357
+ await this.cleanup();
4358
+ process.exit(0);
4359
+ });
4360
+ }
4361
+ async runHeadlessBrowserMode() {
4362
+ const server = await this.httpServerManager.startServer();
4363
+ await this.httpServerManager.waitForServerReady(`http://localhost:${this.config.port}/index.html`, 1e4);
4364
+ this.webSocketManager = new WebSocketManager(this.fileDiscovery, this.config, server, this.consoleReporter);
4365
+ let testSuccess = false;
4366
+ this.webSocketManager.on("testsCompleted", ({ success, coverage }) => {
4367
+ testSuccess = success;
4368
+ if (this.config.coverage) {
4369
+ const cov = new CoverageReportGenerator();
4370
+ cov.generate(coverage);
4371
+ }
4372
+ });
4373
+ const browserType = await this.browserManager.checkBrowser(this.config.browser);
4374
+ if (!browserType) {
4375
+ logger.println("โš ๏ธ Headless browser not available. Falling back to Node.js runner.");
4376
+ this.nodeTestRunner.generateTestRunner();
4377
+ const exitCode = await this.nodeTestRunner.start();
4378
+ await this.cleanup();
4379
+ process.exit(exitCode);
4380
+ }
4381
+ try {
4382
+ await this.browserManager.runHeadlessBrowserTests(browserType, this.config.port);
4383
+ await this.cleanup();
4384
+ process.exit(testSuccess ? 0 : 1);
4385
+ } catch (error) {
4386
+ logger.error(`โŒ Browser test execution failed. Need to install playwright?`);
4387
+ await this.cleanup();
4388
+ process.exit(1);
4389
+ }
4390
+ }
4391
+ async runHeadlessNodeMode() {
4392
+ const exitCode = await this.nodeTestRunner.start();
4393
+ if (this.config.coverage) {
4394
+ const coverage = globalThis.__coverage__;
4395
+ const cov = new CoverageReportGenerator();
4396
+ cov.generate(coverage);
4397
+ }
4398
+ process.exit(exitCode);
4399
+ }
4400
+ async runHeadedBrowserMode() {
4401
+ const server = await this.httpServerManager.startServer();
4402
+ let testsCompleted = false;
4403
+ let testSuccess = false;
4404
+ this.webSocketManager = new WebSocketManager(this.fileDiscovery, this.config, server, this.consoleReporter);
4405
+ logger.println("๐Ÿ“ก WebSocket server ready for real-time test reporting");
4406
+ logger.println("๐Ÿ‘Œ Press Ctrl+C to stop the server");
4407
+ const finishHeadedRun = async (coverage) => {
4408
+ if (this.config.coverage) {
4409
+ const cov = new CoverageReportGenerator();
4410
+ await cov.generate(coverage);
4411
+ }
4412
+ await this.browserManager.closeBrowser();
4413
+ };
4414
+ this.webSocketManager.on("testsCompleted", ({ success, coverage }) => {
4415
+ if (testsCompleted) {
4416
+ return;
4417
+ }
4418
+ testsCompleted = true;
4419
+ testSuccess = success;
4420
+ finishHeadedRun(coverage).catch((error) => {
4421
+ logger.error(`โŒ Failed to finish headed browser run: ${error}`);
4422
+ process.exit(1);
4423
+ });
4424
+ });
4425
+ const onBrowserClose = async () => {
4426
+ const promise = new Promise((resolve) => {
4427
+ if (!testsCompleted) {
4428
+ setImmediate(() => {
4429
+ logger.clearLine();
4430
+ logger.printRaw("\n");
4431
+ logger.clearLine();
4432
+ this.consoleReporter.testsAborted();
4433
+ logger.clearLine();
4434
+ logger.printRaw("\n");
4435
+ logger.println("๐Ÿ”„ Browser window closed prematurely");
4436
+ resolve();
4437
+ });
4438
+ } else {
4439
+ resolve();
4440
+ }
4441
+ });
4442
+ await promise;
4443
+ await this.cleanup();
4444
+ process.exit(testsCompleted ? testSuccess ? 0 : 1 : 1);
4445
+ };
4446
+ await this.browserManager.openBrowser(this.config.port, onBrowserClose);
4447
+ process.once("SIGINT", async () => {
4448
+ if (!testsCompleted) {
4449
+ setImmediate(() => {
4450
+ logger.clearLine();
4451
+ logger.printRaw("\n");
4452
+ logger.clearLine();
4453
+ this.consoleReporter.testsAborted();
4454
+ logger.clearLine();
4455
+ logger.printRaw("\n");
4456
+ logger.printlnRaw("๐Ÿ›‘ Tests aborted by user (Ctrl+C)");
4457
+ });
4458
+ }
4459
+ await this.browserManager.closeBrowser();
4460
+ await this.cleanup();
4461
+ process.exit(testsCompleted ? testSuccess ? 0 : 1 : 1);
4462
+ });
4463
+ }
4464
+ }
4465
+ function createViteJasmineRunner(config) {
4466
+ return new ViteJasmineRunner(config);
4467
+ }
4468
+ class CLIHandler {
4469
+ static async run() {
4470
+ const args = process.argv.slice(2);
4471
+ const helpRequested = args.includes("--help") || args.includes("-h");
4472
+ if (helpRequested) {
4473
+ this.printHelp();
4474
+ return;
4475
+ }
4476
+ const initOnly = args.includes("init");
4477
+ const watch2 = args.includes("--watch");
4478
+ const headless = args.includes("--headless");
4479
+ const coverage = args.includes("--coverage");
4480
+ const browserIndex = args.findIndex((a) => a === "--browser");
4481
+ const seedIndex = args.findIndex((a) => a === "--seed");
4482
+ const silentLogs = args.includes("--silent") || args.includes("--quiet");
4483
+ const hasBrowserArg = browserIndex !== -1;
4484
+ let browserName = "chrome";
4485
+ let seedValue;
4486
+ if (seedIndex !== -1) {
4487
+ const raw = args[seedIndex + 1];
4488
+ const parsed = raw !== void 0 ? Number(raw) : NaN;
4489
+ if (!Number.isFinite(parsed)) {
4490
+ logger.error("ยข?? Invalid --seed value (expected a number).");
4491
+ process.exit(1);
4492
+ }
4493
+ seedValue = parsed;
4494
+ }
4495
+ if (hasBrowserArg && browserIndex + 1 < args.length) {
4496
+ browserName = args[browserIndex + 1];
4497
+ }
4498
+ const preserveOutputsFlag = args.includes("--preserve");
4499
+ const preserveOutputsArg = preserveOutputsFlag ? true : void 0;
4500
+ if (initOnly) {
4501
+ ConfigManager.initViteJasmineConfig();
4502
+ return;
4503
+ }
4504
+ if (watch2) {
4505
+ const invalidFlags = [];
4506
+ if (headless) invalidFlags.push("--headless");
4507
+ if (coverage) invalidFlags.push("--coverage");
4508
+ if (invalidFlags.length > 0) {
4509
+ logger.error(`โŒ The --watch flag cannot be used with: ${invalidFlags.join(", ")}`);
4510
+ process.exit(1);
4511
+ }
4512
+ }
4513
+ try {
4514
+ const normalizeDirConfig = (dirConfig, fallback) => {
4515
+ if (!dirConfig) return [fallback];
4516
+ if (Array.isArray(dirConfig)) {
4517
+ return dirConfig.length > 0 ? dirConfig : [fallback];
4518
+ }
4519
+ return [dirConfig];
4520
+ };
4521
+ let config = ConfigManager.loadViteJasmineBrowserConfig("testify.json");
4522
+ config = {
4523
+ ...config,
4524
+ headless: headless ? true : config.headless || false,
4525
+ coverage: coverage ? true : config.coverage || false,
4526
+ browser: hasBrowserArg ? browserName : config.browser || "chrome",
4527
+ watch: watch2 ? true : config.watch || false,
4528
+ suppressConsoleLogs: silentLogs ? true : config.suppressConsoleLogs,
4529
+ srcDirs: normalizeDirConfig(config.srcDirs, "./src"),
4530
+ testDirs: normalizeDirConfig(config.testDirs, "./tests"),
4531
+ preserveOutputs: preserveOutputsArg ?? !!config.preserveOutputs
4532
+ };
4533
+ if (seedValue !== void 0) {
4534
+ const env = config.jasmineConfig?.env ?? {};
4535
+ config.jasmineConfig = {
4536
+ ...config.jasmineConfig,
4537
+ env: {
4538
+ ...env,
4539
+ seed: seedValue
4540
+ }
4541
+ };
4542
+ }
4543
+ if (config.preserveOutputs) {
4544
+ logger.println(`๐Ÿ›‘ Preserve outputs enabled (skip regenerating index.html and test-runner.js when present).`);
4545
+ }
4546
+ const runner = createViteJasmineRunner(config);
4547
+ if (watch2) {
4548
+ await runner.watch();
4549
+ } else {
4550
+ await runner.start();
4551
+ }
4552
+ } catch (error) {
4553
+ logger.error(`โŒ Failed to start test runner: ${error}`);
4554
+ process.exit(1);
4555
+ }
4556
+ }
4557
+ static printHelp() {
4558
+ logger.println("testify โ€” run your Jasmine tests across browsers, headless, or Node.js.");
4559
+ logger.println("");
4560
+ logger.println("Usage:");
4561
+ logger.println(" npx testify [options]");
4562
+ logger.println(" npx testify init # scaffold testify.json");
4563
+ logger.println("");
4564
+ logger.println("Options:");
4565
+ logger.println(" --headless Run tests in the default Playwright browser without UI");
4566
+ logger.println(" --browser <name> Target browser (chrome|chromium|firefox|webkit)");
4567
+ logger.println(" --watch Launch browser mode + HMR for rapid feedback (cannot be headless)");
4568
+ logger.println(" --coverage Generate Istanbul coverage reports after the run");
4569
+ logger.println(" --seed <number> Seed used for randomization order");
4570
+ logger.println(" --silent / --quiet Suppress console logs when running in Node.js mode");
4571
+ logger.println(" --preserve Skip regenerating index.html and test-runner.js when outputs exist");
4572
+ logger.println(" --help, -h Show this help message");
4573
+ logger.println("");
4574
+ logger.println("Configuration:");
4575
+ logger.println(" testify.json keeps your src/test dirs, browser, port, coverage, and HTML options.");
4576
+ logger.println(" Use --preserve after the first run if you need to debug manually generated assets.");
4577
+ logger.println("");
4578
+ logger.println("Tip:");
4579
+ logger.println(" npx testify --headless --browser node # fastest Node.js test execution");
4580
+ logger.println(" npx testify --headless # run headless Chrome for browser APIs");
4581
+ logger.println("");
4582
+ logger.println("Playwright Browsers:");
4583
+ logger.println(" npx playwright install # install all supported browsers");
4584
+ logger.println(" npx playwright install chromium # install only Chromium");
4585
+ }
4586
+ }
4587
+ class CompoundReporter {
4588
+ reporters;
4589
+ constructor(reporters = []) {
4590
+ this.reporters = reporters;
4591
+ }
4592
+ addReporter(reporter) {
4593
+ this.reporters.push(reporter);
4594
+ }
4595
+ userAgent(agentInfo, suites, specs) {
4596
+ this.reporters.forEach((r) => r?.userAgent?.(agentInfo, suites, specs));
4597
+ }
4598
+ jasmineStarted(suiteInfo) {
4599
+ this.reporters.forEach((r) => r.jasmineStarted?.(suiteInfo));
4600
+ }
4601
+ suiteStarted(result) {
4602
+ this.reporters.forEach((r) => r.suiteStarted?.(result));
4603
+ }
4604
+ specStarted(result) {
4605
+ this.reporters.forEach((r) => r.specStarted?.(result));
4606
+ }
4607
+ specDone(result) {
4608
+ this.reporters.forEach((r) => r.specDone?.(result));
4609
+ }
4610
+ suiteDone(result) {
4611
+ this.reporters.forEach((r) => r.suiteDone?.(result));
4612
+ }
4613
+ jasmineDone(result) {
4614
+ this.reporters.forEach((r) => r.jasmineDone?.(result));
4615
+ }
4616
+ testsAborted(message) {
4617
+ this.reporters.forEach((r) => r?.testsAborted?.(message));
4618
+ }
4619
+ }
4620
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
4621
+ CLIHandler.run();
4622
+ }
4623
+ export {
4624
+ BrowserManager,
4625
+ CLIHandler,
4626
+ CompoundReporter,
4627
+ ConfigManager,
4628
+ ConsoleReporter,
4629
+ FileDiscoveryService,
4630
+ HmrManager,
4631
+ HtmlGenerator,
4632
+ HttpServerManager,
4633
+ IstanbulInstrumenter,
4634
+ Logger,
4635
+ NodeTestRunner,
4636
+ ViteConfigBuilder,
4637
+ ViteJasmineRunner,
4638
+ WebSocketManager,
4639
+ norm
4640
+ };