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.
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ListToolsRequestSchema,
10
+ ReadResourceRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { readFile } from "fs/promises";
13
+ import { existsSync } from "fs";
14
+ var MCPServer = class {
15
+ server;
16
+ config;
17
+ testResults = [];
18
+ constructor(config = {}) {
19
+ this.config = {
20
+ resultsPath: config.resultsPath ?? "./test-results/results.json",
21
+ name: config.name ?? "fair-playwright",
22
+ version: config.version ?? "0.1.0",
23
+ verbose: config.verbose ?? false
24
+ };
25
+ this.server = new Server(
26
+ {
27
+ name: this.config.name,
28
+ version: this.config.version
29
+ },
30
+ {
31
+ capabilities: {
32
+ resources: {},
33
+ tools: {}
34
+ }
35
+ }
36
+ );
37
+ this.setupHandlers();
38
+ }
39
+ /**
40
+ * Setup MCP protocol handlers
41
+ */
42
+ setupHandlers() {
43
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
44
+ resources: [
45
+ {
46
+ uri: "fair-playwright://test-results",
47
+ name: "Test Results",
48
+ description: "Current Playwright test execution results",
49
+ mimeType: "application/json"
50
+ },
51
+ {
52
+ uri: "fair-playwright://test-summary",
53
+ name: "Test Summary",
54
+ description: "AI-optimized summary of test results",
55
+ mimeType: "text/markdown"
56
+ },
57
+ {
58
+ uri: "fair-playwright://failures",
59
+ name: "Failed Tests",
60
+ description: "Detailed information about failed tests",
61
+ mimeType: "text/markdown"
62
+ }
63
+ ]
64
+ }));
65
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
66
+ const uri = request.params.uri.toString();
67
+ if (this.testResults.length === 0) {
68
+ await this.loadTestResults();
69
+ }
70
+ switch (uri) {
71
+ case "fair-playwright://test-results":
72
+ return {
73
+ contents: [
74
+ {
75
+ uri,
76
+ mimeType: "application/json",
77
+ text: JSON.stringify(this.getTestResults(), null, 2)
78
+ }
79
+ ]
80
+ };
81
+ case "fair-playwright://test-summary":
82
+ return {
83
+ contents: [
84
+ {
85
+ uri,
86
+ mimeType: "text/markdown",
87
+ text: this.getTestSummary()
88
+ }
89
+ ]
90
+ };
91
+ case "fair-playwright://failures":
92
+ return {
93
+ contents: [
94
+ {
95
+ uri,
96
+ mimeType: "text/markdown",
97
+ text: this.getFailureSummary()
98
+ }
99
+ ]
100
+ };
101
+ default:
102
+ throw new Error(`Unknown resource: ${uri}`);
103
+ }
104
+ });
105
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
106
+ tools: [
107
+ {
108
+ name: "get_test_results",
109
+ description: "Get complete test execution results with all details",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {}
113
+ }
114
+ },
115
+ {
116
+ name: "get_failure_summary",
117
+ description: "Get AI-optimized summary of failed tests",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {}
121
+ }
122
+ },
123
+ {
124
+ name: "query_test",
125
+ description: "Search for a specific test by title",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ title: {
130
+ type: "string",
131
+ description: "Test title to search for (case-insensitive partial match)"
132
+ }
133
+ },
134
+ required: ["title"]
135
+ }
136
+ },
137
+ {
138
+ name: "get_tests_by_status",
139
+ description: "Get all tests filtered by status",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ status: {
144
+ type: "string",
145
+ enum: ["passed", "failed", "skipped"],
146
+ description: "Test status to filter by"
147
+ }
148
+ },
149
+ required: ["status"]
150
+ }
151
+ },
152
+ {
153
+ name: "get_step_details",
154
+ description: "Get detailed information about test steps",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ testTitle: {
159
+ type: "string",
160
+ description: "Title of the test to get step details for"
161
+ },
162
+ level: {
163
+ type: "string",
164
+ enum: ["major", "minor", "all"],
165
+ description: "Filter steps by level (default: all)"
166
+ }
167
+ },
168
+ required: ["testTitle"]
169
+ }
170
+ }
171
+ ]
172
+ }));
173
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
174
+ const { name, arguments: args } = request.params;
175
+ if (this.testResults.length === 0) {
176
+ await this.loadTestResults();
177
+ }
178
+ switch (name) {
179
+ case "get_test_results":
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: JSON.stringify(this.getTestResults(), null, 2)
185
+ }
186
+ ]
187
+ };
188
+ case "get_failure_summary":
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: this.getFailureSummary()
194
+ }
195
+ ]
196
+ };
197
+ case "query_test": {
198
+ const title = args.title;
199
+ const test = this.queryTest(title);
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: test ? JSON.stringify(test, null, 2) : `No test found matching: ${title}`
205
+ }
206
+ ]
207
+ };
208
+ }
209
+ case "get_tests_by_status": {
210
+ const status = args.status;
211
+ const tests = this.getTestsByStatus(status);
212
+ return {
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: JSON.stringify(tests, null, 2)
217
+ }
218
+ ]
219
+ };
220
+ }
221
+ case "get_step_details": {
222
+ const { testTitle, level } = args;
223
+ const test = this.queryTest(testTitle);
224
+ if (!test) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `No test found matching: ${testTitle}`
230
+ }
231
+ ]
232
+ };
233
+ }
234
+ let steps = test.steps;
235
+ if (level && level !== "all") {
236
+ steps = steps.filter((s) => s.level === level);
237
+ }
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: JSON.stringify(steps, null, 2)
243
+ }
244
+ ]
245
+ };
246
+ }
247
+ default:
248
+ throw new Error(`Unknown tool: ${name}`);
249
+ }
250
+ });
251
+ }
252
+ /**
253
+ * Load test results from JSON file
254
+ */
255
+ async loadTestResults() {
256
+ try {
257
+ if (!existsSync(this.config.resultsPath)) {
258
+ if (this.config.verbose) {
259
+ console.error(`[MCP] Results file not found: ${this.config.resultsPath}`);
260
+ }
261
+ return;
262
+ }
263
+ const content = await readFile(this.config.resultsPath, "utf-8");
264
+ const data = JSON.parse(content);
265
+ this.testResults = Array.isArray(data) ? data : data.tests || [];
266
+ if (this.config.verbose) {
267
+ console.error(`[MCP] Loaded ${this.testResults.length} test results`);
268
+ }
269
+ } catch (error) {
270
+ if (this.config.verbose) {
271
+ console.error(`[MCP] Error loading test results:`, error);
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * Start the MCP server with stdio transport
277
+ */
278
+ async start() {
279
+ const transport = new StdioServerTransport();
280
+ await this.server.connect(transport);
281
+ if (this.config.verbose) {
282
+ console.error("[MCP] Server started and connected via stdio");
283
+ }
284
+ }
285
+ /**
286
+ * Get current test results
287
+ */
288
+ getTestResults() {
289
+ const passed = this.testResults.filter((t) => t.status === "passed").length;
290
+ const failed = this.testResults.filter((t) => t.status === "failed").length;
291
+ const skipped = this.testResults.filter((t) => t.status === "skipped").length;
292
+ const totalDuration = this.testResults.reduce((sum, t) => sum + (t.duration || 0), 0);
293
+ return {
294
+ status: failed > 0 ? "failed" : this.testResults.length > 0 ? "passed" : "unknown",
295
+ summary: {
296
+ total: this.testResults.length,
297
+ passed,
298
+ failed,
299
+ skipped,
300
+ duration: totalDuration
301
+ },
302
+ tests: this.testResults
303
+ };
304
+ }
305
+ /**
306
+ * Get test summary in markdown format
307
+ */
308
+ getTestSummary() {
309
+ const results = this.getTestResults();
310
+ const { summary } = results;
311
+ let md = "# Playwright Test Results\n\n";
312
+ md += `**Status**: ${results.status === "failed" ? "\u274C FAILED" : "\u2705 PASSED"}
313
+ `;
314
+ md += `**Total Tests**: ${summary.total}
315
+ `;
316
+ md += `**Duration**: ${(summary.duration / 1e3).toFixed(2)}s
317
+
318
+ `;
319
+ md += "## Summary\n\n";
320
+ md += `- \u2705 Passed: ${summary.passed}
321
+ `;
322
+ md += `- \u274C Failed: ${summary.failed}
323
+ `;
324
+ md += `- \u2298 Skipped: ${summary.skipped}
325
+
326
+ `;
327
+ if (summary.failed > 0) {
328
+ md += "## Failed Tests\n\n";
329
+ const failedTests = this.testResults.filter((t) => t.status === "failed");
330
+ failedTests.forEach((test, index) => {
331
+ md += `${index + 1}. **${test.title}** (${test.duration}ms)
332
+ `;
333
+ md += ` - File: \`${test.file}\`
334
+ `;
335
+ if (test.error) {
336
+ md += ` - Error: ${test.error.message}
337
+ `;
338
+ }
339
+ });
340
+ }
341
+ return md;
342
+ }
343
+ /**
344
+ * Get AI-optimized summary of failures
345
+ */
346
+ getFailureSummary() {
347
+ const failedTests = this.testResults.filter((t) => t.status === "failed");
348
+ if (failedTests.length === 0) {
349
+ return "# No Test Failures\n\nAll tests passed! \u2705";
350
+ }
351
+ let summary = `# Test Failures Summary
352
+
353
+ `;
354
+ summary += `${failedTests.length} test(s) failed:
355
+
356
+ `;
357
+ failedTests.forEach((test, index) => {
358
+ summary += `## ${index + 1}. ${test.title}
359
+
360
+ `;
361
+ summary += `**File**: \`${test.file}\`
362
+ `;
363
+ summary += `**Duration**: ${test.duration}ms
364
+
365
+ `;
366
+ if (test.error) {
367
+ summary += `**Error**: ${test.error.message}
368
+
369
+ `;
370
+ if (test.error.location) {
371
+ summary += `**Location**: \`${test.error.location}\`
372
+
373
+ `;
374
+ }
375
+ }
376
+ const majorSteps = test.steps.filter((s) => s.level === "major" && !s.parentId);
377
+ if (majorSteps.length > 0) {
378
+ summary += `**Test Flow**:
379
+
380
+ `;
381
+ majorSteps.forEach((majorStep, idx) => {
382
+ const icon = majorStep.status === "passed" ? "\u2705" : "\u274C";
383
+ summary += `${idx + 1}. ${icon} [MAJOR] ${majorStep.title}
384
+ `;
385
+ if (majorStep.status === "failed") {
386
+ const minorSteps = test.steps.filter((s) => s.parentId === majorStep.id);
387
+ minorSteps.forEach((minorStep) => {
388
+ const minorIcon = minorStep.status === "passed" ? "\u2705" : "\u274C";
389
+ summary += ` - ${minorIcon} [minor] ${minorStep.title}
390
+ `;
391
+ if (minorStep.error) {
392
+ summary += ` Error: ${minorStep.error.message}
393
+ `;
394
+ }
395
+ });
396
+ }
397
+ });
398
+ summary += `
399
+ `;
400
+ }
401
+ if (test.consoleErrors && test.consoleErrors.length > 0) {
402
+ summary += `**Browser Console Errors** (${test.consoleErrors.length}):
403
+
404
+ `;
405
+ test.consoleErrors.forEach((consoleError, idx) => {
406
+ summary += `${idx + 1}. [${consoleError.type}] ${consoleError.message}
407
+ `;
408
+ });
409
+ summary += `
410
+ `;
411
+ }
412
+ summary += `---
413
+
414
+ `;
415
+ });
416
+ return summary;
417
+ }
418
+ /**
419
+ * Query specific test by title
420
+ */
421
+ queryTest(title) {
422
+ const test = this.testResults.find(
423
+ (t) => t.title.toLowerCase().includes(title.toLowerCase())
424
+ );
425
+ return test || null;
426
+ }
427
+ /**
428
+ * Get tests by status
429
+ */
430
+ getTestsByStatus(status) {
431
+ return this.testResults.filter((t) => t.status === status);
432
+ }
433
+ };
434
+ async function createMCPServer(config) {
435
+ const server = new MCPServer(config);
436
+ await server.start();
437
+ return server;
438
+ }
439
+
440
+ // src/mcp/cli.ts
441
+ async function main() {
442
+ const args = process.argv.slice(2);
443
+ const config = {};
444
+ for (let i = 0; i < args.length; i++) {
445
+ const arg = args[i];
446
+ switch (arg) {
447
+ case "--help":
448
+ case "-h":
449
+ console.log(`
450
+ fair-playwright MCP Server
451
+
452
+ Usage:
453
+ npx fair-playwright-mcp [options]
454
+
455
+ Options:
456
+ --results-path <path> Path to test results JSON file
457
+ Default: ./test-results/results.json
458
+ --verbose Enable verbose logging
459
+ --help Show this help message
460
+
461
+ Example:
462
+ npx fair-playwright-mcp --results-path ./custom-results.json
463
+
464
+ For Claude Desktop integration, add to claude_desktop_config.json:
465
+ {
466
+ "mcpServers": {
467
+ "fair-playwright": {
468
+ "command": "npx",
469
+ "args": ["fair-playwright-mcp"]
470
+ }
471
+ }
472
+ }
473
+ `);
474
+ process.exit(0);
475
+ break;
476
+ case "--results-path":
477
+ config.resultsPath = args[++i];
478
+ break;
479
+ case "--verbose":
480
+ config.verbose = true;
481
+ break;
482
+ default:
483
+ console.error(`Unknown option: ${arg}`);
484
+ console.error(`Run with --help for usage information`);
485
+ process.exit(1);
486
+ }
487
+ }
488
+ if (process.env.FAIR_PLAYWRIGHT_RESULTS && !config.resultsPath) {
489
+ config.resultsPath = process.env.FAIR_PLAYWRIGHT_RESULTS;
490
+ }
491
+ try {
492
+ await createMCPServer(config);
493
+ process.on("SIGINT", () => {
494
+ if (config.verbose) {
495
+ console.error("\n[MCP] Shutting down...");
496
+ }
497
+ process.exit(0);
498
+ });
499
+ } catch (error) {
500
+ console.error("Failed to start MCP server:", error);
501
+ process.exit(1);
502
+ }
503
+ }
504
+ main();
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "fair-playwright",
3
+ "version": "1.0.0",
4
+ "description": "AI-optimized Playwright test reporter with progressive terminal output and hierarchical step management",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "fair-playwright-mcp": "./dist/mcp-cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "build:watch": "tsup --watch",
27
+ "dev": "tsup --watch",
28
+ "test": "vitest",
29
+ "test:watch": "vitest --watch",
30
+ "test:integration": "npm run build && cd test-project && npm test",
31
+ "lint": "eslint src --ext .ts",
32
+ "format": "prettier --write \"src/**/*.ts\"",
33
+ "format:check": "prettier --check \"src/**/*.ts\"",
34
+ "typecheck": "tsc --noEmit",
35
+ "changeset": "changeset",
36
+ "release": "changeset publish",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "keywords": [
40
+ "playwright",
41
+ "testing",
42
+ "reporter",
43
+ "ai",
44
+ "test-automation",
45
+ "e2e",
46
+ "terminal",
47
+ "progressive",
48
+ "ai-optimized",
49
+ "mcp",
50
+ "model-context-protocol",
51
+ "llm",
52
+ "claude"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/baranaytass/fair-playwright.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/baranaytass/fair-playwright/issues"
60
+ },
61
+ "homepage": "https://github.com/baranaytass/fair-playwright#readme",
62
+ "author": "Baran Aytas",
63
+ "license": "MIT",
64
+ "peerDependencies": {
65
+ "@playwright/test": ">=1.40.0"
66
+ },
67
+ "dependencies": {
68
+ "@modelcontextprotocol/sdk": "^1.25.1",
69
+ "log-update": "^6.1.0",
70
+ "picocolors": "^1.1.1"
71
+ },
72
+ "devDependencies": {
73
+ "@changesets/cli": "^2.27.10",
74
+ "@playwright/test": "^1.49.1",
75
+ "@types/node": "^22.10.2",
76
+ "@typescript-eslint/eslint-plugin": "^8.18.1",
77
+ "@typescript-eslint/parser": "^8.18.1",
78
+ "eslint": "^9.17.0",
79
+ "eslint-config-prettier": "^9.1.0",
80
+ "prettier": "^3.4.2",
81
+ "tsup": "^8.3.5",
82
+ "typescript": "^5.7.2",
83
+ "vitest": "^2.1.8"
84
+ },
85
+ "engines": {
86
+ "node": ">=18"
87
+ }
88
+ }