fair-playwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1387 @@
1
+ // src/reporter/StepTracker.ts
2
+ var StepTracker = class {
3
+ tests = /* @__PURE__ */ new Map();
4
+ steps = /* @__PURE__ */ new Map();
5
+ durationThreshold;
6
+ autoDetect;
7
+ constructor(options) {
8
+ this.durationThreshold = options?.durationThreshold ?? 1e3;
9
+ this.autoDetect = options?.autoDetect ?? true;
10
+ }
11
+ /**
12
+ * Start tracking a test
13
+ */
14
+ startTest(testCase, _result) {
15
+ const testId = this.getTestId(testCase);
16
+ const test2 = {
17
+ id: testId,
18
+ title: testCase.title,
19
+ file: testCase.location.file,
20
+ status: "running",
21
+ startTime: Date.now(),
22
+ steps: [],
23
+ attachments: [],
24
+ consoleErrors: []
25
+ };
26
+ this.tests.set(testId, test2);
27
+ return testId;
28
+ }
29
+ /**
30
+ * Start tracking a step
31
+ */
32
+ startStep(testCase, _result, step, parentStepId) {
33
+ const testId = this.getTestId(testCase);
34
+ const stepId = this.getStepId(testCase, step);
35
+ const stepMetadata = {
36
+ id: stepId,
37
+ title: this.cleanStepTitle(step.title),
38
+ level: this.classifyStep(step, parentStepId),
39
+ status: "running",
40
+ startTime: Date.now(),
41
+ parentId: parentStepId,
42
+ childIds: []
43
+ };
44
+ this.steps.set(stepId, stepMetadata);
45
+ if (parentStepId) {
46
+ const parent = this.steps.get(parentStepId);
47
+ if (parent) {
48
+ parent.childIds.push(stepId);
49
+ }
50
+ }
51
+ const test2 = this.tests.get(testId);
52
+ if (test2) {
53
+ test2.steps.push(stepMetadata);
54
+ }
55
+ return stepId;
56
+ }
57
+ /**
58
+ * End tracking a step
59
+ */
60
+ endStep(_testCase, _result, step, stepId) {
61
+ const stepMetadata = this.steps.get(stepId);
62
+ if (!stepMetadata) return;
63
+ stepMetadata.endTime = Date.now();
64
+ stepMetadata.duration = stepMetadata.endTime - stepMetadata.startTime;
65
+ stepMetadata.status = step.error ? "failed" : "passed";
66
+ if (step.error) {
67
+ stepMetadata.error = {
68
+ message: step.error.message || "Unknown error",
69
+ stack: step.error.stack
70
+ };
71
+ }
72
+ if (this.autoDetect && stepMetadata.duration > this.durationThreshold) {
73
+ stepMetadata.level = "major";
74
+ }
75
+ }
76
+ /**
77
+ * End tracking a test
78
+ */
79
+ endTest(testCase, result) {
80
+ const testId = this.getTestId(testCase);
81
+ const test2 = this.tests.get(testId);
82
+ if (!test2) return;
83
+ test2.endTime = Date.now();
84
+ test2.duration = result.duration;
85
+ test2.status = this.mapTestStatus(result.status);
86
+ if (result.error) {
87
+ test2.error = {
88
+ message: result.error.message || "Unknown error",
89
+ stack: result.error.stack,
90
+ location: `${testCase.location.file}:${testCase.location.line}:${testCase.location.column}`
91
+ };
92
+ }
93
+ test2.attachments = result.attachments.map((att) => ({
94
+ name: att.name,
95
+ path: att.path,
96
+ contentType: att.contentType
97
+ }));
98
+ this.extractConsoleErrors(test2, result);
99
+ }
100
+ /**
101
+ * Extract console errors from test result output
102
+ */
103
+ extractConsoleErrors(test2, result) {
104
+ if (!test2.consoleErrors) {
105
+ test2.consoleErrors = [];
106
+ }
107
+ if (!result.stdout || !result.stderr) {
108
+ return;
109
+ }
110
+ const output = result.stdout.join("\n") + "\n" + result.stderr.join("\n");
111
+ const errorPatterns = [
112
+ /console\.error:\s*(.+)/gi,
113
+ /\[error\]\s*(.+)/gi,
114
+ /ERROR:\s*(.+)/gi
115
+ ];
116
+ errorPatterns.forEach((pattern) => {
117
+ const matches = output.matchAll(pattern);
118
+ for (const match of matches) {
119
+ if (match[1]) {
120
+ test2.consoleErrors.push({
121
+ type: "error",
122
+ message: match[1].trim(),
123
+ timestamp: Date.now()
124
+ });
125
+ }
126
+ }
127
+ });
128
+ }
129
+ /**
130
+ * Get test metadata
131
+ */
132
+ getTest(testId) {
133
+ return this.tests.get(testId);
134
+ }
135
+ /**
136
+ * Get all tests
137
+ */
138
+ getAllTests() {
139
+ return Array.from(this.tests.values());
140
+ }
141
+ /**
142
+ * Get step metadata
143
+ */
144
+ getStep(stepId) {
145
+ return this.steps.get(stepId);
146
+ }
147
+ /**
148
+ * Clear all tracking data
149
+ */
150
+ clear() {
151
+ this.tests.clear();
152
+ this.steps.clear();
153
+ }
154
+ /**
155
+ * Generate unique test ID
156
+ */
157
+ getTestId(testCase) {
158
+ return `${testCase.location.file}::${testCase.title}`;
159
+ }
160
+ /**
161
+ * Generate unique step ID
162
+ */
163
+ getStepId(testCase, step) {
164
+ return `${this.getTestId(testCase)}::${step.title}::${Date.now()}`;
165
+ }
166
+ /**
167
+ * Classify step as MAJOR or MINOR
168
+ */
169
+ classifyStep(step, parentStepId) {
170
+ const prefixMatch = step.title.match(/^\[(MAJOR|MINOR)\]\s*/);
171
+ if (prefixMatch) {
172
+ return prefixMatch[1].toLowerCase();
173
+ }
174
+ if (parentStepId) {
175
+ return "minor";
176
+ }
177
+ const majorKeywords = ["login", "checkout", "payment", "register", "setup", "flow"];
178
+ const titleLower = step.title.toLowerCase();
179
+ if (majorKeywords.some((keyword) => titleLower.includes(keyword))) {
180
+ return "major";
181
+ }
182
+ return "minor";
183
+ }
184
+ /**
185
+ * Clean step title by removing [MAJOR]/[MINOR] prefix
186
+ */
187
+ cleanStepTitle(title) {
188
+ return title.replace(/^\[(MAJOR|MINOR)\]\s*/, "");
189
+ }
190
+ /**
191
+ * Map Playwright test status to our status
192
+ */
193
+ mapTestStatus(status) {
194
+ switch (status) {
195
+ case "passed":
196
+ return "passed";
197
+ case "failed":
198
+ return "failed";
199
+ case "skipped":
200
+ return "skipped";
201
+ case "timedOut":
202
+ return "failed";
203
+ default:
204
+ return "failed";
205
+ }
206
+ }
207
+ };
208
+
209
+ // src/formatters/ConsoleFormatter.ts
210
+ import pc from "picocolors";
211
+ import logUpdate from "log-update";
212
+ var ConsoleFormatter = class {
213
+ config;
214
+ totalTests = 0;
215
+ completedTests = 0;
216
+ passedTests = 0;
217
+ failedTests = 0;
218
+ skippedTests = 0;
219
+ runningSteps = /* @__PURE__ */ new Map();
220
+ isCI;
221
+ useProgressiveMode;
222
+ updateTimer;
223
+ constructor(config) {
224
+ this.config = config;
225
+ this.isCI = this.detectCI();
226
+ this.useProgressiveMode = config.mode === "progressive" && !this.isCI && process.stdout.isTTY === true;
227
+ }
228
+ /**
229
+ * Detect if running in CI environment
230
+ */
231
+ detectCI() {
232
+ return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.JENKINS_URL);
233
+ }
234
+ onBegin(totalTests) {
235
+ this.totalTests = totalTests;
236
+ const header = pc.bold(pc.blue("\u{1F3AD} Fair Playwright Reporter"));
237
+ const subheader = pc.dim(`Running ${totalTests} test(s)...`);
238
+ if (this.useProgressiveMode) {
239
+ console.log(`
240
+ ${header}
241
+ ${subheader}
242
+ `);
243
+ } else {
244
+ console.log(`
245
+ ${header}
246
+ ${subheader}
247
+ `);
248
+ }
249
+ }
250
+ onTestBegin(test2) {
251
+ if (!this.useProgressiveMode && this.config.mode !== "minimal") {
252
+ console.log(pc.dim(`\u23F3 ${test2.title}`));
253
+ }
254
+ }
255
+ onStepBegin(step) {
256
+ this.runningSteps.set(step.id, step);
257
+ if (this.useProgressiveMode) {
258
+ this.scheduleUpdate();
259
+ } else if (this.config.mode === "full") {
260
+ const indent = step.parentId ? " " : " ";
261
+ const icon = step.level === "major" ? "\u25B6" : "\u25B8";
262
+ console.log(pc.dim(`${indent}${icon} ${step.title}...`));
263
+ }
264
+ }
265
+ onStepEnd(step) {
266
+ this.runningSteps.delete(step.id);
267
+ if (this.useProgressiveMode) {
268
+ this.scheduleUpdate();
269
+ } else if (this.config.mode === "full") {
270
+ const indent = step.parentId ? " " : " ";
271
+ const icon = step.status === "passed" ? pc.green("\u2713") : pc.red("\u2717");
272
+ const duration = step.duration ? pc.dim(` (${step.duration}ms)`) : "";
273
+ const levelBadge = step.level === "major" ? pc.blue("[MAJOR]") : pc.dim("[minor]");
274
+ console.log(`${indent}${icon} ${levelBadge} ${step.title}${duration}`);
275
+ }
276
+ }
277
+ onTestEnd(test2) {
278
+ this.completedTests++;
279
+ if (test2.status === "passed") {
280
+ this.passedTests++;
281
+ } else if (test2.status === "failed") {
282
+ this.failedTests++;
283
+ } else if (test2.status === "skipped") {
284
+ this.skippedTests++;
285
+ }
286
+ if (this.useProgressiveMode) {
287
+ if (test2.status === "failed") {
288
+ this.clearUpdate();
289
+ this.printFailedTest(test2);
290
+ }
291
+ this.scheduleUpdate();
292
+ } else {
293
+ if (test2.status === "passed") {
294
+ if (this.config.compression.passedTests !== "hide") {
295
+ const icon = pc.green("\u2713");
296
+ const duration = pc.dim(` (${test2.duration}ms)`);
297
+ console.log(`${icon} ${pc.dim(test2.title)}${duration}`);
298
+ }
299
+ } else if (test2.status === "failed") {
300
+ this.printFailedTest(test2);
301
+ } else if (test2.status === "skipped") {
302
+ console.log(pc.yellow(`\u2298 ${test2.title} (skipped)`));
303
+ }
304
+ }
305
+ }
306
+ onEnd(allTests, _result) {
307
+ if (this.useProgressiveMode) {
308
+ this.clearUpdate();
309
+ if (this.updateTimer) {
310
+ clearTimeout(this.updateTimer);
311
+ }
312
+ }
313
+ console.log("");
314
+ console.log(pc.bold("\u2500".repeat(60)));
315
+ console.log("");
316
+ const passed = allTests.filter((t) => t.status === "passed").length;
317
+ const failed = allTests.filter((t) => t.status === "failed").length;
318
+ const skipped = allTests.filter((t) => t.status === "skipped").length;
319
+ const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
320
+ if (failed > 0) {
321
+ console.log(pc.red(pc.bold(`\u2717 ${failed} failed`)));
322
+ }
323
+ if (passed > 0) {
324
+ console.log(pc.green(`\u2713 ${passed} passed`));
325
+ }
326
+ if (skipped > 0) {
327
+ console.log(pc.yellow(`\u2298 ${skipped} skipped`));
328
+ }
329
+ console.log(pc.dim(`
330
+ Total: ${allTests.length} test(s)`));
331
+ console.log(pc.dim(`Duration: ${(totalDuration / 1e3).toFixed(2)}s`));
332
+ if (this.config.output.ai) {
333
+ const aiPath = typeof this.config.output.ai === "string" ? this.config.output.ai : "./test-results/ai-summary.md";
334
+ console.log(pc.dim(`
335
+ \u{1F4DD} AI Summary: ${aiPath}`));
336
+ }
337
+ console.log("");
338
+ }
339
+ onError(error) {
340
+ if (this.useProgressiveMode) {
341
+ this.clearUpdate();
342
+ }
343
+ console.error(pc.red(`
344
+ \u274C Reporter Error: ${error.message}`));
345
+ if (error.stack) {
346
+ console.error(pc.dim(error.stack));
347
+ }
348
+ }
349
+ /**
350
+ * Schedule a progressive update
351
+ */
352
+ scheduleUpdate() {
353
+ if (!this.useProgressiveMode) return;
354
+ if (this.updateTimer) {
355
+ clearTimeout(this.updateTimer);
356
+ }
357
+ this.updateTimer = setTimeout(() => {
358
+ this.render();
359
+ }, this.config.progressive.updateInterval);
360
+ }
361
+ /**
362
+ * Render progressive output
363
+ */
364
+ render() {
365
+ if (!this.useProgressiveMode) return;
366
+ const lines = [];
367
+ const progress = this.totalTests > 0 ? Math.floor(this.completedTests / this.totalTests * 100) : 0;
368
+ lines.push(
369
+ pc.dim(`Progress: ${this.completedTests}/${this.totalTests} tests `) + pc.green(`(${progress}%)`)
370
+ );
371
+ const statusParts = [];
372
+ if (this.passedTests > 0) statusParts.push(pc.green(`\u2713 ${this.passedTests}`));
373
+ if (this.failedTests > 0) statusParts.push(pc.red(`\u2717 ${this.failedTests}`));
374
+ if (this.skippedTests > 0) statusParts.push(pc.yellow(`\u2298 ${this.skippedTests}`));
375
+ if (statusParts.length > 0) {
376
+ lines.push(statusParts.join(" "));
377
+ }
378
+ if (this.runningSteps.size > 0) {
379
+ lines.push("");
380
+ lines.push(pc.dim("Running:"));
381
+ const runningStepsArray = Array.from(this.runningSteps.values());
382
+ runningStepsArray.slice(0, 5).forEach((step) => {
383
+ const indent = step.parentId ? " " : " ";
384
+ const icon = step.level === "major" ? "\u25B6" : "\u25B8";
385
+ const levelBadge = step.level === "major" ? pc.blue("[MAJOR]") : pc.dim("[minor]");
386
+ const elapsed = Date.now() - step.startTime;
387
+ lines.push(`${indent}${icon} ${levelBadge} ${step.title} ${pc.dim(`(${elapsed}ms)`)}`);
388
+ });
389
+ if (runningStepsArray.length > 5) {
390
+ lines.push(pc.dim(` ... and ${runningStepsArray.length - 5} more`));
391
+ }
392
+ }
393
+ logUpdate(lines.join("\n"));
394
+ }
395
+ /**
396
+ * Clear progressive update
397
+ */
398
+ clearUpdate() {
399
+ if (this.useProgressiveMode) {
400
+ logUpdate.clear();
401
+ }
402
+ }
403
+ /**
404
+ * Print failed test details with progressive MAJOR step display
405
+ */
406
+ printFailedTest(test2) {
407
+ console.log(pc.red(`\u2717 ${test2.title}`));
408
+ console.log("");
409
+ const majorSteps = test2.steps.filter((s) => s.level === "major" && !s.parentId);
410
+ if (majorSteps.length > 0) {
411
+ majorSteps.forEach((majorStep, index) => {
412
+ const stepNumber = index + 1;
413
+ if (majorStep.status === "passed") {
414
+ const duration = majorStep.duration ? pc.dim(` (${majorStep.duration}ms)`) : "";
415
+ const successMsg = majorStep.successMessage ? pc.dim(` - ${majorStep.successMessage}`) : "";
416
+ console.log(pc.green(` ${stepNumber}. \u2713 [MAJOR] ${majorStep.title}${successMsg}${duration}`));
417
+ } else if (majorStep.status === "failed") {
418
+ console.log(pc.red(` ${stepNumber}. \u2717 [MAJOR] ${majorStep.title}`));
419
+ const minorSteps = test2.steps.filter((s) => s.parentId === majorStep.id);
420
+ if (minorSteps.length > 0) {
421
+ minorSteps.forEach((minorStep, minorIndex) => {
422
+ const minorNumber = minorIndex + 1;
423
+ const duration = minorStep.duration ? pc.dim(` (${minorStep.duration}ms)`) : "";
424
+ if (minorStep.status === "passed") {
425
+ console.log(pc.green(` ${minorNumber}. \u2713 [minor] ${minorStep.title}${duration}`));
426
+ } else if (minorStep.status === "failed") {
427
+ console.log(pc.red(` ${minorNumber}. \u2717 [minor] ${minorStep.title}${duration}`));
428
+ if (minorStep.error) {
429
+ console.log(pc.dim(` ${minorStep.error.message}`));
430
+ }
431
+ } else {
432
+ console.log(pc.dim(` ${minorNumber}. \u2298 [minor] ${minorStep.title}`));
433
+ }
434
+ });
435
+ }
436
+ if (majorStep.error) {
437
+ console.log("");
438
+ console.log(pc.red(` Error: ${majorStep.error.message}`));
439
+ }
440
+ } else if (majorStep.status === "skipped") {
441
+ console.log(pc.yellow(` ${stepNumber}. \u2298 [MAJOR] ${majorStep.title} (skipped)`));
442
+ }
443
+ });
444
+ }
445
+ if (test2.error) {
446
+ console.log("");
447
+ console.log(pc.red(` Error Message: ${test2.error.message}`));
448
+ if (test2.error.stack) {
449
+ console.log("");
450
+ console.log(pc.dim(" Stack Trace:"));
451
+ const stackLines = test2.error.stack.split("\n").slice(0, 5);
452
+ stackLines.forEach((line) => {
453
+ console.log(pc.dim(` ${line}`));
454
+ });
455
+ }
456
+ if (test2.error.location) {
457
+ console.log(pc.dim(` Location: ${test2.error.location}`));
458
+ }
459
+ }
460
+ if (test2.consoleErrors && test2.consoleErrors.length > 0) {
461
+ console.log("");
462
+ console.log(pc.yellow(` Browser Console Errors (${test2.consoleErrors.length}):`));
463
+ test2.consoleErrors.forEach((consoleError, index) => {
464
+ console.log(pc.yellow(` ${index + 1}. [${consoleError.type}] ${consoleError.message}`));
465
+ if (consoleError.location) {
466
+ console.log(pc.dim(` at ${consoleError.location}`));
467
+ }
468
+ });
469
+ }
470
+ console.log("");
471
+ }
472
+ };
473
+
474
+ // src/formatters/AIFormatter.ts
475
+ import { writeFile, mkdir } from "fs/promises";
476
+ import { dirname } from "path";
477
+ var AIFormatter = class {
478
+ config;
479
+ outputPath;
480
+ constructor(config, outputPath) {
481
+ this.config = config;
482
+ this.outputPath = outputPath;
483
+ }
484
+ async write(allTests, _result) {
485
+ const markdown = this.generateMarkdown(allTests, _result);
486
+ try {
487
+ await mkdir(dirname(this.outputPath), { recursive: true });
488
+ await writeFile(this.outputPath, markdown, "utf-8");
489
+ } catch (error) {
490
+ console.error(`Failed to write AI summary: ${error}`);
491
+ }
492
+ }
493
+ generateMarkdown(allTests, _result) {
494
+ const passed = allTests.filter((t) => t.status === "passed").length;
495
+ const failed = allTests.filter((t) => t.status === "failed").length;
496
+ const skipped = allTests.filter((t) => t.status === "skipped").length;
497
+ const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
498
+ const overallStatus = failed > 0 ? "\u274C FAILED" : "\u2705 PASSED";
499
+ let md = `# Test Results
500
+
501
+ `;
502
+ md += `**Status**: ${overallStatus} (${passed}/${allTests.length} tests passed)
503
+ `;
504
+ md += `**Duration**: ${(totalDuration / 1e3).toFixed(2)}s
505
+ `;
506
+ md += `**Date**: ${(/* @__PURE__ */ new Date()).toISOString()}
507
+
508
+ `;
509
+ md += `## Summary
510
+
511
+ `;
512
+ md += `- \u2705 Passed: ${passed}
513
+ `;
514
+ md += `- \u274C Failed: ${failed}
515
+ `;
516
+ md += `- \u2298 Skipped: ${skipped}
517
+ `;
518
+ md += `- \u{1F4CA} Total: ${allTests.length}
519
+
520
+ `;
521
+ const failedTests = allTests.filter((t) => t.status === "failed");
522
+ if (failedTests.length > 0) {
523
+ md += `## \u274C Failed Tests
524
+
525
+ `;
526
+ failedTests.forEach((test2) => {
527
+ md += `### ${test2.title}
528
+
529
+ `;
530
+ md += `**File**: \`${test2.file}\`
531
+ `;
532
+ md += `**Duration**: ${test2.duration}ms
533
+
534
+ `;
535
+ if (test2.error) {
536
+ md += `**Error**: ${test2.error.message}
537
+
538
+ `;
539
+ if (test2.error.location) {
540
+ md += `**Location**: \`${test2.error.location}\`
541
+
542
+ `;
543
+ }
544
+ }
545
+ if (test2.steps.length > 0) {
546
+ md += `**Steps Executed**:
547
+
548
+ `;
549
+ test2.steps.forEach((step, index) => {
550
+ const icon = step.status === "passed" ? "\u2705" : step.status === "failed" ? "\u274C" : "\u2298";
551
+ const level = step.level === "major" ? "**MAJOR**" : "minor";
552
+ const duration = step.duration ? ` (${step.duration}ms)` : "";
553
+ md += `${index + 1}. ${icon} [${level}] ${step.title}${duration}
554
+ `;
555
+ if (step.status === "failed" && step.error) {
556
+ md += ` - Error: ${step.error.message}
557
+ `;
558
+ }
559
+ });
560
+ md += `
561
+ `;
562
+ }
563
+ if (test2.consoleErrors && test2.consoleErrors.length > 0) {
564
+ md += `**Browser Console Errors** (${test2.consoleErrors.length}):
565
+
566
+ `;
567
+ test2.consoleErrors.forEach((consoleError, index) => {
568
+ md += `${index + 1}. **[${consoleError.type}]** ${consoleError.message}
569
+ `;
570
+ if (consoleError.location) {
571
+ md += ` - Location: \`${consoleError.location}\`
572
+ `;
573
+ }
574
+ });
575
+ md += `
576
+ `;
577
+ }
578
+ if (test2.attachments.length > 0) {
579
+ md += `**Artifacts**:
580
+
581
+ `;
582
+ test2.attachments.forEach((att) => {
583
+ if (att.path) {
584
+ md += `- ${att.name}: \`${att.path}\`
585
+ `;
586
+ }
587
+ });
588
+ md += `
589
+ `;
590
+ }
591
+ md += `---
592
+
593
+ `;
594
+ });
595
+ }
596
+ if (this.config.compression.passedTests !== "hide") {
597
+ const passedTests = allTests.filter((t) => t.status === "passed");
598
+ if (passedTests.length > 0) {
599
+ md += `## \u2705 Passed Tests
600
+
601
+ `;
602
+ if (this.config.compression.passedTests === "summary") {
603
+ md += `All ${passedTests.length} test(s) passed:
604
+
605
+ `;
606
+ passedTests.forEach((test2) => {
607
+ md += `- ${test2.title} (${test2.duration}ms)
608
+ `;
609
+ });
610
+ md += `
611
+ `;
612
+ } else {
613
+ passedTests.forEach((test2) => {
614
+ md += `### ${test2.title}
615
+
616
+ `;
617
+ md += `**Duration**: ${test2.duration}ms
618
+
619
+ `;
620
+ if (test2.steps.length > 0) {
621
+ md += `**Steps**:
622
+
623
+ `;
624
+ test2.steps.forEach((step, index) => {
625
+ md += `${index + 1}. \u2705 ${step.title} (${step.duration}ms)
626
+ `;
627
+ });
628
+ }
629
+ md += `
630
+ `;
631
+ });
632
+ }
633
+ }
634
+ }
635
+ const skippedTests = allTests.filter((t) => t.status === "skipped");
636
+ if (skippedTests.length > 0) {
637
+ md += `## \u2298 Skipped Tests
638
+
639
+ `;
640
+ skippedTests.forEach((test2) => {
641
+ md += `- ${test2.title}
642
+ `;
643
+ });
644
+ md += `
645
+ `;
646
+ }
647
+ md += `---
648
+
649
+ `;
650
+ md += `*Generated by [fair-playwright](https://github.com/baranaytas/fair-playwright)*
651
+ `;
652
+ return md;
653
+ }
654
+ };
655
+
656
+ // src/formatters/JSONFormatter.ts
657
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
658
+ import { dirname as dirname2 } from "path";
659
+ var JSONFormatter = class {
660
+ outputPath;
661
+ constructor(_config, outputPath) {
662
+ this.outputPath = outputPath;
663
+ }
664
+ async write(allTests, _result) {
665
+ const passed = allTests.filter((t) => t.status === "passed").length;
666
+ const failed = allTests.filter((t) => t.status === "failed").length;
667
+ const skipped = allTests.filter((t) => t.status === "skipped").length;
668
+ const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
669
+ const output = {
670
+ status: failed > 0 ? "failed" : "passed",
671
+ summary: {
672
+ total: allTests.length,
673
+ passed,
674
+ failed,
675
+ skipped,
676
+ duration: totalDuration
677
+ },
678
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
679
+ tests: allTests.map((test2) => ({
680
+ id: test2.id,
681
+ title: test2.title,
682
+ file: test2.file,
683
+ status: test2.status,
684
+ duration: test2.duration,
685
+ startTime: test2.startTime,
686
+ endTime: test2.endTime,
687
+ error: test2.error ? {
688
+ message: test2.error.message,
689
+ stack: test2.error.stack,
690
+ location: test2.error.location
691
+ } : void 0,
692
+ steps: test2.steps.map((step) => ({
693
+ id: step.id,
694
+ title: step.title,
695
+ level: step.level,
696
+ status: step.status,
697
+ duration: step.duration,
698
+ error: step.error ? {
699
+ message: step.error.message,
700
+ stack: step.error.stack
701
+ } : void 0
702
+ })),
703
+ attachments: test2.attachments
704
+ }))
705
+ };
706
+ try {
707
+ await mkdir2(dirname2(this.outputPath), { recursive: true });
708
+ await writeFile2(this.outputPath, JSON.stringify(output, null, 2), "utf-8");
709
+ } catch (error) {
710
+ console.error(`Failed to write JSON output: ${error}`);
711
+ }
712
+ }
713
+ };
714
+
715
+ // src/reporter/FairReporter.ts
716
+ var FairReporter = class {
717
+ config;
718
+ stepTracker;
719
+ consoleFormatter;
720
+ aiFormatter;
721
+ jsonFormatter;
722
+ stepIdMap = /* @__PURE__ */ new Map();
723
+ constructor(config) {
724
+ this.config = {
725
+ mode: config?.mode ?? "progressive",
726
+ aiOptimized: config?.aiOptimized ?? true,
727
+ output: {
728
+ console: config?.output?.console ?? true,
729
+ ai: config?.output?.ai ?? false,
730
+ json: config?.output?.json ?? false
731
+ },
732
+ stepClassification: {
733
+ durationThreshold: config?.stepClassification?.durationThreshold ?? 1e3,
734
+ autoDetect: config?.stepClassification?.autoDetect ?? true
735
+ },
736
+ progressive: {
737
+ clearCompleted: config?.progressive?.clearCompleted ?? true,
738
+ updateInterval: config?.progressive?.updateInterval ?? 100
739
+ },
740
+ compression: {
741
+ passedTests: config?.compression?.passedTests ?? "summary",
742
+ failureContext: {
743
+ steps: config?.compression?.failureContext?.steps ?? 3,
744
+ screenshot: config?.compression?.failureContext?.screenshot ?? true,
745
+ trace: config?.compression?.failureContext?.trace ?? true,
746
+ logs: config?.compression?.failureContext?.logs ?? true
747
+ }
748
+ }
749
+ };
750
+ this.stepTracker = new StepTracker({
751
+ durationThreshold: this.config.stepClassification.durationThreshold,
752
+ autoDetect: this.config.stepClassification.autoDetect
753
+ });
754
+ if (this.config.output.console) {
755
+ this.consoleFormatter = new ConsoleFormatter(this.config);
756
+ }
757
+ if (this.config.output.ai) {
758
+ const outputPath = typeof this.config.output.ai === "string" ? this.config.output.ai : "./test-results/ai-summary.md";
759
+ this.aiFormatter = new AIFormatter(this.config, outputPath);
760
+ }
761
+ if (this.config.output.json) {
762
+ const outputPath = typeof this.config.output.json === "string" ? this.config.output.json : "./test-results/results.json";
763
+ this.jsonFormatter = new JSONFormatter(this.config, outputPath);
764
+ }
765
+ }
766
+ /**
767
+ * Called once before running tests
768
+ */
769
+ onBegin(_config, suite) {
770
+ const totalTests = suite.allTests().length;
771
+ if (this.consoleFormatter) {
772
+ this.consoleFormatter.onBegin(totalTests);
773
+ }
774
+ }
775
+ /**
776
+ * Called when a test begins
777
+ */
778
+ onTestBegin(test2, result) {
779
+ this.stepTracker.startTest(test2, result);
780
+ if (this.consoleFormatter) {
781
+ this.consoleFormatter.onTestBegin(test2);
782
+ }
783
+ }
784
+ /**
785
+ * Called when a test step begins
786
+ */
787
+ onStepBegin(test2, result, step) {
788
+ let parentStepId;
789
+ if (step.parent) {
790
+ parentStepId = this.stepIdMap.get(step.parent);
791
+ }
792
+ const stepId = this.stepTracker.startStep(test2, result, step, parentStepId);
793
+ this.stepIdMap.set(step, stepId);
794
+ if (this.consoleFormatter) {
795
+ const stepMetadata = this.stepTracker.getStep(stepId);
796
+ if (stepMetadata) {
797
+ this.consoleFormatter.onStepBegin(stepMetadata);
798
+ }
799
+ }
800
+ }
801
+ /**
802
+ * Called when a test step ends
803
+ */
804
+ onStepEnd(test2, result, step) {
805
+ const stepId = this.stepIdMap.get(step);
806
+ if (!stepId) return;
807
+ this.stepTracker.endStep(test2, result, step, stepId);
808
+ if (this.consoleFormatter) {
809
+ const stepMetadata = this.stepTracker.getStep(stepId);
810
+ if (stepMetadata) {
811
+ this.consoleFormatter.onStepEnd(stepMetadata);
812
+ }
813
+ }
814
+ }
815
+ /**
816
+ * Called when a test ends
817
+ */
818
+ onTestEnd(test2, result) {
819
+ this.stepTracker.endTest(test2, result);
820
+ if (this.consoleFormatter) {
821
+ const testId = `${test2.location.file}::${test2.title}`;
822
+ const testMetadata = this.stepTracker.getTest(testId);
823
+ if (testMetadata) {
824
+ this.consoleFormatter.onTestEnd(testMetadata);
825
+ }
826
+ }
827
+ }
828
+ /**
829
+ * Called once after all tests have finished
830
+ */
831
+ async onEnd(result) {
832
+ const allTests = this.stepTracker.getAllTests();
833
+ if (this.consoleFormatter) {
834
+ this.consoleFormatter.onEnd(allTests, result);
835
+ }
836
+ if (this.aiFormatter) {
837
+ await this.aiFormatter.write(allTests, result);
838
+ }
839
+ if (this.jsonFormatter) {
840
+ await this.jsonFormatter.write(allTests, result);
841
+ }
842
+ this.stepIdMap.clear();
843
+ }
844
+ /**
845
+ * Optional: Called on error
846
+ */
847
+ onError(error) {
848
+ if (this.consoleFormatter) {
849
+ this.consoleFormatter.onError(error);
850
+ }
851
+ }
852
+ };
853
+
854
+ // src/e2e.ts
855
+ import { test } from "@playwright/test";
856
+ var E2EHelperImpl = class {
857
+ /**
858
+ * Execute a major step
859
+ * Supports both inline and declarative modes
860
+ */
861
+ async major(title, actionOrOptions, options) {
862
+ if (typeof actionOrOptions === "object" && "steps" in actionOrOptions) {
863
+ return this.majorDeclarative(title, actionOrOptions);
864
+ }
865
+ if (typeof actionOrOptions === "function") {
866
+ return this.majorInline(title, actionOrOptions, options);
867
+ }
868
+ throw new Error("Invalid arguments for e2e.major()");
869
+ }
870
+ /**
871
+ * Execute a minor step (inline mode only)
872
+ */
873
+ async minor(title, action, options) {
874
+ const stepTitle = this.formatStepTitle(title, "minor", options);
875
+ return test.step(stepTitle, async () => {
876
+ try {
877
+ await action();
878
+ } catch (error) {
879
+ if (options?.failure) {
880
+ const enhancedError = new Error(`${options.failure}: ${error.message}`);
881
+ enhancedError.stack = error.stack;
882
+ throw enhancedError;
883
+ }
884
+ throw error;
885
+ }
886
+ });
887
+ }
888
+ /**
889
+ * Inline mode for major steps
890
+ */
891
+ async majorInline(title, action, options) {
892
+ const stepTitle = this.formatStepTitle(title, "major", options);
893
+ return test.step(stepTitle, async () => {
894
+ try {
895
+ await action();
896
+ } catch (error) {
897
+ if (options?.failure) {
898
+ const enhancedError = new Error(`${options.failure}: ${error.message}`);
899
+ enhancedError.stack = error.stack;
900
+ throw enhancedError;
901
+ }
902
+ throw error;
903
+ }
904
+ });
905
+ }
906
+ /**
907
+ * Declarative mode for major steps with child steps
908
+ */
909
+ async majorDeclarative(title, options) {
910
+ const stepTitle = this.formatStepTitle(title, "major", options);
911
+ return test.step(stepTitle, async () => {
912
+ if (!options.steps || options.steps.length === 0) {
913
+ return;
914
+ }
915
+ for (const childStep of options.steps) {
916
+ const childTitle = this.formatStepTitle(childStep.title, "minor", childStep);
917
+ await test.step(childTitle, async () => {
918
+ try {
919
+ await childStep.action();
920
+ } catch (error) {
921
+ if (childStep.failure) {
922
+ const enhancedError = new Error(
923
+ `${childStep.failure}: ${error.message}`
924
+ );
925
+ enhancedError.stack = error.stack;
926
+ throw enhancedError;
927
+ }
928
+ throw error;
929
+ }
930
+ });
931
+ }
932
+ });
933
+ }
934
+ /**
935
+ * Format step title with metadata for the reporter
936
+ * The reporter will parse this metadata to classify steps
937
+ */
938
+ formatStepTitle(title, level, _options) {
939
+ const prefix = level === "major" ? "[MAJOR]" : "[MINOR]";
940
+ return `${prefix} ${title}`;
941
+ }
942
+ };
943
+ var e2e = new E2EHelperImpl();
944
+
945
+ // src/mcp/server.ts
946
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
947
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
948
+ import {
949
+ CallToolRequestSchema,
950
+ ListResourcesRequestSchema,
951
+ ListToolsRequestSchema,
952
+ ReadResourceRequestSchema
953
+ } from "@modelcontextprotocol/sdk/types.js";
954
+ import { readFile } from "fs/promises";
955
+ import { existsSync } from "fs";
956
+ var MCPServer = class {
957
+ server;
958
+ config;
959
+ testResults = [];
960
+ constructor(config = {}) {
961
+ this.config = {
962
+ resultsPath: config.resultsPath ?? "./test-results/results.json",
963
+ name: config.name ?? "fair-playwright",
964
+ version: config.version ?? "0.1.0",
965
+ verbose: config.verbose ?? false
966
+ };
967
+ this.server = new Server(
968
+ {
969
+ name: this.config.name,
970
+ version: this.config.version
971
+ },
972
+ {
973
+ capabilities: {
974
+ resources: {},
975
+ tools: {}
976
+ }
977
+ }
978
+ );
979
+ this.setupHandlers();
980
+ }
981
+ /**
982
+ * Setup MCP protocol handlers
983
+ */
984
+ setupHandlers() {
985
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
986
+ resources: [
987
+ {
988
+ uri: "fair-playwright://test-results",
989
+ name: "Test Results",
990
+ description: "Current Playwright test execution results",
991
+ mimeType: "application/json"
992
+ },
993
+ {
994
+ uri: "fair-playwright://test-summary",
995
+ name: "Test Summary",
996
+ description: "AI-optimized summary of test results",
997
+ mimeType: "text/markdown"
998
+ },
999
+ {
1000
+ uri: "fair-playwright://failures",
1001
+ name: "Failed Tests",
1002
+ description: "Detailed information about failed tests",
1003
+ mimeType: "text/markdown"
1004
+ }
1005
+ ]
1006
+ }));
1007
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1008
+ const uri = request.params.uri.toString();
1009
+ if (this.testResults.length === 0) {
1010
+ await this.loadTestResults();
1011
+ }
1012
+ switch (uri) {
1013
+ case "fair-playwright://test-results":
1014
+ return {
1015
+ contents: [
1016
+ {
1017
+ uri,
1018
+ mimeType: "application/json",
1019
+ text: JSON.stringify(this.getTestResults(), null, 2)
1020
+ }
1021
+ ]
1022
+ };
1023
+ case "fair-playwright://test-summary":
1024
+ return {
1025
+ contents: [
1026
+ {
1027
+ uri,
1028
+ mimeType: "text/markdown",
1029
+ text: this.getTestSummary()
1030
+ }
1031
+ ]
1032
+ };
1033
+ case "fair-playwright://failures":
1034
+ return {
1035
+ contents: [
1036
+ {
1037
+ uri,
1038
+ mimeType: "text/markdown",
1039
+ text: this.getFailureSummary()
1040
+ }
1041
+ ]
1042
+ };
1043
+ default:
1044
+ throw new Error(`Unknown resource: ${uri}`);
1045
+ }
1046
+ });
1047
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
1048
+ tools: [
1049
+ {
1050
+ name: "get_test_results",
1051
+ description: "Get complete test execution results with all details",
1052
+ inputSchema: {
1053
+ type: "object",
1054
+ properties: {}
1055
+ }
1056
+ },
1057
+ {
1058
+ name: "get_failure_summary",
1059
+ description: "Get AI-optimized summary of failed tests",
1060
+ inputSchema: {
1061
+ type: "object",
1062
+ properties: {}
1063
+ }
1064
+ },
1065
+ {
1066
+ name: "query_test",
1067
+ description: "Search for a specific test by title",
1068
+ inputSchema: {
1069
+ type: "object",
1070
+ properties: {
1071
+ title: {
1072
+ type: "string",
1073
+ description: "Test title to search for (case-insensitive partial match)"
1074
+ }
1075
+ },
1076
+ required: ["title"]
1077
+ }
1078
+ },
1079
+ {
1080
+ name: "get_tests_by_status",
1081
+ description: "Get all tests filtered by status",
1082
+ inputSchema: {
1083
+ type: "object",
1084
+ properties: {
1085
+ status: {
1086
+ type: "string",
1087
+ enum: ["passed", "failed", "skipped"],
1088
+ description: "Test status to filter by"
1089
+ }
1090
+ },
1091
+ required: ["status"]
1092
+ }
1093
+ },
1094
+ {
1095
+ name: "get_step_details",
1096
+ description: "Get detailed information about test steps",
1097
+ inputSchema: {
1098
+ type: "object",
1099
+ properties: {
1100
+ testTitle: {
1101
+ type: "string",
1102
+ description: "Title of the test to get step details for"
1103
+ },
1104
+ level: {
1105
+ type: "string",
1106
+ enum: ["major", "minor", "all"],
1107
+ description: "Filter steps by level (default: all)"
1108
+ }
1109
+ },
1110
+ required: ["testTitle"]
1111
+ }
1112
+ }
1113
+ ]
1114
+ }));
1115
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1116
+ const { name, arguments: args } = request.params;
1117
+ if (this.testResults.length === 0) {
1118
+ await this.loadTestResults();
1119
+ }
1120
+ switch (name) {
1121
+ case "get_test_results":
1122
+ return {
1123
+ content: [
1124
+ {
1125
+ type: "text",
1126
+ text: JSON.stringify(this.getTestResults(), null, 2)
1127
+ }
1128
+ ]
1129
+ };
1130
+ case "get_failure_summary":
1131
+ return {
1132
+ content: [
1133
+ {
1134
+ type: "text",
1135
+ text: this.getFailureSummary()
1136
+ }
1137
+ ]
1138
+ };
1139
+ case "query_test": {
1140
+ const title = args.title;
1141
+ const test2 = this.queryTest(title);
1142
+ return {
1143
+ content: [
1144
+ {
1145
+ type: "text",
1146
+ text: test2 ? JSON.stringify(test2, null, 2) : `No test found matching: ${title}`
1147
+ }
1148
+ ]
1149
+ };
1150
+ }
1151
+ case "get_tests_by_status": {
1152
+ const status = args.status;
1153
+ const tests = this.getTestsByStatus(status);
1154
+ return {
1155
+ content: [
1156
+ {
1157
+ type: "text",
1158
+ text: JSON.stringify(tests, null, 2)
1159
+ }
1160
+ ]
1161
+ };
1162
+ }
1163
+ case "get_step_details": {
1164
+ const { testTitle, level } = args;
1165
+ const test2 = this.queryTest(testTitle);
1166
+ if (!test2) {
1167
+ return {
1168
+ content: [
1169
+ {
1170
+ type: "text",
1171
+ text: `No test found matching: ${testTitle}`
1172
+ }
1173
+ ]
1174
+ };
1175
+ }
1176
+ let steps = test2.steps;
1177
+ if (level && level !== "all") {
1178
+ steps = steps.filter((s) => s.level === level);
1179
+ }
1180
+ return {
1181
+ content: [
1182
+ {
1183
+ type: "text",
1184
+ text: JSON.stringify(steps, null, 2)
1185
+ }
1186
+ ]
1187
+ };
1188
+ }
1189
+ default:
1190
+ throw new Error(`Unknown tool: ${name}`);
1191
+ }
1192
+ });
1193
+ }
1194
+ /**
1195
+ * Load test results from JSON file
1196
+ */
1197
+ async loadTestResults() {
1198
+ try {
1199
+ if (!existsSync(this.config.resultsPath)) {
1200
+ if (this.config.verbose) {
1201
+ console.error(`[MCP] Results file not found: ${this.config.resultsPath}`);
1202
+ }
1203
+ return;
1204
+ }
1205
+ const content = await readFile(this.config.resultsPath, "utf-8");
1206
+ const data = JSON.parse(content);
1207
+ this.testResults = Array.isArray(data) ? data : data.tests || [];
1208
+ if (this.config.verbose) {
1209
+ console.error(`[MCP] Loaded ${this.testResults.length} test results`);
1210
+ }
1211
+ } catch (error) {
1212
+ if (this.config.verbose) {
1213
+ console.error(`[MCP] Error loading test results:`, error);
1214
+ }
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Start the MCP server with stdio transport
1219
+ */
1220
+ async start() {
1221
+ const transport = new StdioServerTransport();
1222
+ await this.server.connect(transport);
1223
+ if (this.config.verbose) {
1224
+ console.error("[MCP] Server started and connected via stdio");
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Get current test results
1229
+ */
1230
+ getTestResults() {
1231
+ const passed = this.testResults.filter((t) => t.status === "passed").length;
1232
+ const failed = this.testResults.filter((t) => t.status === "failed").length;
1233
+ const skipped = this.testResults.filter((t) => t.status === "skipped").length;
1234
+ const totalDuration = this.testResults.reduce((sum, t) => sum + (t.duration || 0), 0);
1235
+ return {
1236
+ status: failed > 0 ? "failed" : this.testResults.length > 0 ? "passed" : "unknown",
1237
+ summary: {
1238
+ total: this.testResults.length,
1239
+ passed,
1240
+ failed,
1241
+ skipped,
1242
+ duration: totalDuration
1243
+ },
1244
+ tests: this.testResults
1245
+ };
1246
+ }
1247
+ /**
1248
+ * Get test summary in markdown format
1249
+ */
1250
+ getTestSummary() {
1251
+ const results = this.getTestResults();
1252
+ const { summary } = results;
1253
+ let md = "# Playwright Test Results\n\n";
1254
+ md += `**Status**: ${results.status === "failed" ? "\u274C FAILED" : "\u2705 PASSED"}
1255
+ `;
1256
+ md += `**Total Tests**: ${summary.total}
1257
+ `;
1258
+ md += `**Duration**: ${(summary.duration / 1e3).toFixed(2)}s
1259
+
1260
+ `;
1261
+ md += "## Summary\n\n";
1262
+ md += `- \u2705 Passed: ${summary.passed}
1263
+ `;
1264
+ md += `- \u274C Failed: ${summary.failed}
1265
+ `;
1266
+ md += `- \u2298 Skipped: ${summary.skipped}
1267
+
1268
+ `;
1269
+ if (summary.failed > 0) {
1270
+ md += "## Failed Tests\n\n";
1271
+ const failedTests = this.testResults.filter((t) => t.status === "failed");
1272
+ failedTests.forEach((test2, index) => {
1273
+ md += `${index + 1}. **${test2.title}** (${test2.duration}ms)
1274
+ `;
1275
+ md += ` - File: \`${test2.file}\`
1276
+ `;
1277
+ if (test2.error) {
1278
+ md += ` - Error: ${test2.error.message}
1279
+ `;
1280
+ }
1281
+ });
1282
+ }
1283
+ return md;
1284
+ }
1285
+ /**
1286
+ * Get AI-optimized summary of failures
1287
+ */
1288
+ getFailureSummary() {
1289
+ const failedTests = this.testResults.filter((t) => t.status === "failed");
1290
+ if (failedTests.length === 0) {
1291
+ return "# No Test Failures\n\nAll tests passed! \u2705";
1292
+ }
1293
+ let summary = `# Test Failures Summary
1294
+
1295
+ `;
1296
+ summary += `${failedTests.length} test(s) failed:
1297
+
1298
+ `;
1299
+ failedTests.forEach((test2, index) => {
1300
+ summary += `## ${index + 1}. ${test2.title}
1301
+
1302
+ `;
1303
+ summary += `**File**: \`${test2.file}\`
1304
+ `;
1305
+ summary += `**Duration**: ${test2.duration}ms
1306
+
1307
+ `;
1308
+ if (test2.error) {
1309
+ summary += `**Error**: ${test2.error.message}
1310
+
1311
+ `;
1312
+ if (test2.error.location) {
1313
+ summary += `**Location**: \`${test2.error.location}\`
1314
+
1315
+ `;
1316
+ }
1317
+ }
1318
+ const majorSteps = test2.steps.filter((s) => s.level === "major" && !s.parentId);
1319
+ if (majorSteps.length > 0) {
1320
+ summary += `**Test Flow**:
1321
+
1322
+ `;
1323
+ majorSteps.forEach((majorStep, idx) => {
1324
+ const icon = majorStep.status === "passed" ? "\u2705" : "\u274C";
1325
+ summary += `${idx + 1}. ${icon} [MAJOR] ${majorStep.title}
1326
+ `;
1327
+ if (majorStep.status === "failed") {
1328
+ const minorSteps = test2.steps.filter((s) => s.parentId === majorStep.id);
1329
+ minorSteps.forEach((minorStep) => {
1330
+ const minorIcon = minorStep.status === "passed" ? "\u2705" : "\u274C";
1331
+ summary += ` - ${minorIcon} [minor] ${minorStep.title}
1332
+ `;
1333
+ if (minorStep.error) {
1334
+ summary += ` Error: ${minorStep.error.message}
1335
+ `;
1336
+ }
1337
+ });
1338
+ }
1339
+ });
1340
+ summary += `
1341
+ `;
1342
+ }
1343
+ if (test2.consoleErrors && test2.consoleErrors.length > 0) {
1344
+ summary += `**Browser Console Errors** (${test2.consoleErrors.length}):
1345
+
1346
+ `;
1347
+ test2.consoleErrors.forEach((consoleError, idx) => {
1348
+ summary += `${idx + 1}. [${consoleError.type}] ${consoleError.message}
1349
+ `;
1350
+ });
1351
+ summary += `
1352
+ `;
1353
+ }
1354
+ summary += `---
1355
+
1356
+ `;
1357
+ });
1358
+ return summary;
1359
+ }
1360
+ /**
1361
+ * Query specific test by title
1362
+ */
1363
+ queryTest(title) {
1364
+ const test2 = this.testResults.find(
1365
+ (t) => t.title.toLowerCase().includes(title.toLowerCase())
1366
+ );
1367
+ return test2 || null;
1368
+ }
1369
+ /**
1370
+ * Get tests by status
1371
+ */
1372
+ getTestsByStatus(status) {
1373
+ return this.testResults.filter((t) => t.status === status);
1374
+ }
1375
+ };
1376
+ async function createMCPServer(config) {
1377
+ const server = new MCPServer(config);
1378
+ await server.start();
1379
+ return server;
1380
+ }
1381
+ export {
1382
+ FairReporter,
1383
+ MCPServer,
1384
+ createMCPServer,
1385
+ FairReporter as default,
1386
+ e2e
1387
+ };