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