playwright-dashboard-reporter 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/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # playwright-dashboard-reporter
2
+
3
+ Official Playwright reporter for [YShvydak Test Dashboard](https://github.com/shvydak/yshvydak-test-dashboard) - a full-stack testing dashboard with real-time monitoring, one-click reruns, and comprehensive test reporting.
4
+
5
+ ## Features
6
+
7
+ - ๐Ÿ”„ Real-time test execution monitoring via WebSocket
8
+ - ๐Ÿ“Š Comprehensive test result tracking with execution history
9
+ - ๐Ÿ“Ž Automatic attachment management (videos, screenshots, traces)
10
+ - ๐Ÿ” Enhanced error reporting with code context and line highlighting
11
+ - ๐ŸŽฏ Stable test ID generation for reliable test tracking
12
+ - ๐Ÿ—๏ธ Built-in diagnostics and health checks
13
+ - ๐Ÿš€ Zero configuration - works out of the box
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install --save-dev playwright-dashboard-reporter
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Add Reporter to Playwright Config
24
+
25
+ ```typescript
26
+ // playwright.config.ts
27
+ import { defineConfig } from '@playwright/test';
28
+
29
+ export default defineConfig({
30
+ reporter: [
31
+ ['playwright-dashboard-reporter', {
32
+ apiBaseUrl: process.env.DASHBOARD_API_URL || 'http://localhost:3001'
33
+ }],
34
+ ['html'] // Keep your existing reporters
35
+ ]
36
+ });
37
+ ```
38
+
39
+ ### 2. Set Environment Variable
40
+
41
+ ```bash
42
+ # .env
43
+ DASHBOARD_API_URL=http://localhost:3001
44
+ ```
45
+
46
+ ### 3. Start Dashboard Server
47
+
48
+ ```bash
49
+ # Clone dashboard repository
50
+ git clone https://github.com/shvydak/yshvydak-test-dashboard.git
51
+ cd yshvydak-test-dashboard
52
+
53
+ # Install and start
54
+ npm install
55
+ npm run dev
56
+ ```
57
+
58
+ The dashboard will be available at:
59
+ - ๐ŸŒ Web UI: http://localhost:3000
60
+ - ๐Ÿ”Œ API: http://localhost:3001
61
+
62
+ ### 4. Run Your Tests
63
+
64
+ ```bash
65
+ npx playwright test
66
+ ```
67
+
68
+ Results will appear in your Dashboard automatically! ๐ŸŽ‰
69
+
70
+ ## Configuration Options
71
+
72
+ ```typescript
73
+ interface ReporterOptions {
74
+ apiBaseUrl?: string; // Dashboard API URL (default: http://localhost:3001)
75
+ silent?: boolean; // Suppress console output (default: false)
76
+ timeout?: number; // API request timeout in ms (default: 30000)
77
+ }
78
+ ```
79
+
80
+ ### Example with Custom Options
81
+
82
+ ```typescript
83
+ // playwright.config.ts
84
+ export default defineConfig({
85
+ reporter: [
86
+ ['playwright-dashboard-reporter', {
87
+ apiBaseUrl: 'https://dashboard.mycompany.com',
88
+ silent: true,
89
+ timeout: 60000
90
+ }]
91
+ ]
92
+ });
93
+ ```
94
+
95
+ ## Environment Variables
96
+
97
+ The reporter supports the following environment variables:
98
+
99
+ ```bash
100
+ # Required: Dashboard API endpoint
101
+ DASHBOARD_API_URL=http://localhost:3001
102
+
103
+ # Optional: Silent mode (suppresses console output)
104
+ YSHVYDAK_REPORTER_SILENT=true
105
+
106
+ # Optional: Custom timeout in milliseconds
107
+ YSHVYDAK_REPORTER_TIMEOUT=60000
108
+ ```
109
+
110
+ ## Usage Scenarios
111
+
112
+ ### Production Deployment
113
+
114
+ ```bash
115
+ # Set production Dashboard URL
116
+ export DASHBOARD_API_URL=https://dashboard.mycompany.com
117
+
118
+ # Run tests
119
+ npx playwright test
120
+ ```
121
+
122
+ ### CI/CD Integration
123
+
124
+ ```yaml
125
+ # GitHub Actions example
126
+ - name: Run Playwright Tests
127
+ env:
128
+ DASHBOARD_API_URL: ${{ secrets.DASHBOARD_URL }}
129
+ run: npx playwright test
130
+ ```
131
+
132
+ ### Multiple Projects
133
+
134
+ The reporter works seamlessly across multiple Playwright projects:
135
+
136
+ ```bash
137
+ # Project 1
138
+ cd /path/to/project1
139
+ npm install --save-dev playwright-dashboard-reporter
140
+ npx playwright test
141
+
142
+ # Project 2
143
+ cd /path/to/project2
144
+ npm install --save-dev playwright-dashboard-reporter
145
+ npx playwright test
146
+
147
+ # All results appear in the same Dashboard!
148
+ ```
149
+
150
+ ## Dashboard Features
151
+
152
+ When using this reporter, you get access to:
153
+
154
+ - โœ… **Real-time Test Monitoring** - Watch tests execute live
155
+ - ๐Ÿ”„ **One-Click Reruns** - Rerun failed tests instantly
156
+ - ๐Ÿ“ˆ **Flaky Test Detection** - Identify unstable tests automatically
157
+ - ๐Ÿ“Š **Timeline Visualization** - View execution trends over time
158
+ - ๐ŸŽฅ **Attachment Viewer** - Watch videos, view screenshots, analyze traces
159
+ - ๐Ÿ“œ **Execution History** - Track all test runs with complete data
160
+ - ๐Ÿ” **Test Discovery** - Automatically detect all available tests
161
+ - ๐ŸŽฏ **Detailed Reporting** - Enhanced error messages with code context
162
+
163
+ ## Troubleshooting
164
+
165
+ ### Reporter Not Sending Data
166
+
167
+ **Symptom:** Tests run but no data appears in Dashboard
168
+
169
+ **Solution:**
170
+ 1. Verify Dashboard server is running:
171
+ ```bash
172
+ curl http://localhost:3001/api/health
173
+ ```
174
+ 2. Check `DASHBOARD_API_URL` environment variable:
175
+ ```bash
176
+ echo $DASHBOARD_API_URL
177
+ ```
178
+ 3. Run diagnostics:
179
+ ```bash
180
+ curl http://localhost:3001/api/tests/diagnostics
181
+ ```
182
+
183
+ ### Connection Timeout
184
+
185
+ **Symptom:** Reporter shows timeout errors
186
+
187
+ **Solution:** Increase timeout in reporter configuration:
188
+
189
+ ```typescript
190
+ reporter: [
191
+ ['playwright-dashboard-reporter', {
192
+ timeout: 60000 // 60 seconds
193
+ }]
194
+ ]
195
+ ```
196
+
197
+ ### Silent Mode Not Working
198
+
199
+ **Symptom:** Reporter still outputs to console
200
+
201
+ **Solution:** Set environment variable explicitly:
202
+
203
+ ```bash
204
+ export YSHVYDAK_REPORTER_SILENT=true
205
+ npx playwright test
206
+ ```
207
+
208
+ ## API Compatibility
209
+
210
+ This reporter is compatible with:
211
+ - Dashboard API version: **1.x and above**
212
+ - Playwright version: **1.40.0 and above**
213
+ - Node.js version: **18.0.0 and above**
214
+
215
+ ## Development
216
+
217
+ ### Local Development Setup
218
+
219
+ If you're developing the Dashboard and want to test reporter changes:
220
+
221
+ ```bash
222
+ # 1. Link reporter locally
223
+ cd /path/to/yshvydak-test-dashboard/packages/reporter
224
+ npm link
225
+
226
+ # 2. Use linked reporter in test project
227
+ cd /path/to/your-test-project
228
+ npm link playwright-dashboard-reporter
229
+
230
+ # 3. Watch for changes (auto-rebuild)
231
+ cd /path/to/yshvydak-test-dashboard/packages/reporter
232
+ npm run dev
233
+ ```
234
+
235
+ ### Building from Source
236
+
237
+ ```bash
238
+ cd packages/reporter
239
+ npm run build # Build distribution
240
+ npm run type-check # TypeScript validation
241
+ ```
242
+
243
+ ## Support
244
+
245
+ - ๐Ÿ“š [Documentation](https://github.com/shvydak/yshvydak-test-dashboard/tree/main/docs)
246
+ - ๐Ÿ› [Report Issues](https://github.com/shvydak/yshvydak-test-dashboard/issues)
247
+ - ๐Ÿ’ฌ [Discussions](https://github.com/shvydak/yshvydak-test-dashboard/discussions)
248
+
249
+ ## Contributing
250
+
251
+ Contributions are welcome! See [CONTRIBUTING.md](https://github.com/shvydak/yshvydak-test-dashboard/blob/main/CONTRIBUTING.md) for guidelines.
252
+
253
+ ## License
254
+
255
+ MIT ยฉ YShvydak
256
+
257
+ ---
258
+
259
+ **Made with โค๏ธ for the Playwright community**
@@ -0,0 +1,27 @@
1
+ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
+
3
+ declare class YShvydakReporter implements Reporter {
4
+ private runId;
5
+ private results;
6
+ private startTime;
7
+ private apiBaseUrl;
8
+ constructor();
9
+ onBegin(_config: FullConfig, suite: Suite): void;
10
+ onTestEnd(test: TestCase, result: TestResult): void;
11
+ onEnd(result: FullResult): Promise<void>;
12
+ private generateStableTestId;
13
+ private mapStatus;
14
+ private processAttachments;
15
+ private createEnhancedErrorMessage;
16
+ private findCaretPosition;
17
+ private getStatusIcon;
18
+ private sendTestResult;
19
+ private createTestRun;
20
+ private updateTestRun;
21
+ private notifyProcessStart;
22
+ private notifyProcessEnd;
23
+ private setupCleanupHandlers;
24
+ private cleanup;
25
+ }
26
+
27
+ export { YShvydakReporter as default };
@@ -0,0 +1,27 @@
1
+ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
+
3
+ declare class YShvydakReporter implements Reporter {
4
+ private runId;
5
+ private results;
6
+ private startTime;
7
+ private apiBaseUrl;
8
+ constructor();
9
+ onBegin(_config: FullConfig, suite: Suite): void;
10
+ onTestEnd(test: TestCase, result: TestResult): void;
11
+ onEnd(result: FullResult): Promise<void>;
12
+ private generateStableTestId;
13
+ private mapStatus;
14
+ private processAttachments;
15
+ private createEnhancedErrorMessage;
16
+ private findCaretPosition;
17
+ private getStatusIcon;
18
+ private sendTestResult;
19
+ private createTestRun;
20
+ private updateTestRun;
21
+ private notifyProcessStart;
22
+ private notifyProcessEnd;
23
+ private setupCleanupHandlers;
24
+ private cleanup;
25
+ }
26
+
27
+ export { YShvydakReporter as default };
package/dist/index.js ADDED
@@ -0,0 +1,368 @@
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
+ default: () => index_default
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var path = __toESM(require("path"));
37
+ var fs = __toESM(require("fs"));
38
+ var import_uuid = require("uuid");
39
+ var dotenv = __toESM(require("dotenv"));
40
+ dotenv.config();
41
+ var YShvydakReporter = class {
42
+ constructor() {
43
+ this.runId = (0, import_uuid.v4)();
44
+ this.results = [];
45
+ this.startTime = 0;
46
+ let baseUrl = process.env.DASHBOARD_API_URL || "http://localhost:3001";
47
+ if (baseUrl.endsWith("/api")) {
48
+ baseUrl = baseUrl.slice(0, -4);
49
+ }
50
+ this.apiBaseUrl = baseUrl;
51
+ console.log(`\u{1F3AD} YShvydak Dashboard Reporter initialized (Run ID: ${this.runId})`);
52
+ console.log(`\u{1F310} API Base URL: ${this.apiBaseUrl}`);
53
+ if (!this.apiBaseUrl || this.apiBaseUrl === "undefined") {
54
+ console.warn(
55
+ `\u26A0\uFE0F Dashboard API URL not configured! Using fallback: http://localhost:3001`
56
+ );
57
+ this.apiBaseUrl = "http://localhost:3001";
58
+ }
59
+ this.setupCleanupHandlers();
60
+ }
61
+ onBegin(_config, suite) {
62
+ this.startTime = Date.now();
63
+ this.notifyProcessStart({
64
+ runId: this.runId,
65
+ type: "run-all",
66
+ totalTests: suite.allTests().length
67
+ });
68
+ this.createTestRun({
69
+ id: this.runId,
70
+ status: "running",
71
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
72
+ totalTests: suite.allTests().length,
73
+ passedTests: 0,
74
+ failedTests: 0,
75
+ skippedTests: 0,
76
+ duration: 0
77
+ });
78
+ console.log(`\u{1F680} Starting test run with ${suite.allTests().length} tests`);
79
+ }
80
+ onTestEnd(test, result) {
81
+ const testId = this.generateStableTestId(test);
82
+ const filePath = path.relative(process.cwd(), test.location.file);
83
+ let enhancedErrorMessage = result.error?.stack || result.error?.message;
84
+ if (result.status === "failed" && result.error) {
85
+ enhancedErrorMessage = this.createEnhancedErrorMessage(test, result.error);
86
+ }
87
+ const testResult = {
88
+ id: (0, import_uuid.v4)(),
89
+ testId,
90
+ runId: this.runId,
91
+ name: test.title,
92
+ filePath,
93
+ status: this.mapStatus(result.status),
94
+ duration: result.duration,
95
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
96
+ errorMessage: enhancedErrorMessage,
97
+ errorStack: result.error?.stack,
98
+ attachments: this.processAttachments(result.attachments)
99
+ };
100
+ this.results.push(testResult);
101
+ this.sendTestResult(testResult);
102
+ console.log(
103
+ `${this.getStatusIcon(testResult.status)} ${testResult.name} (${testResult.duration}ms)`
104
+ );
105
+ }
106
+ async onEnd(result) {
107
+ const duration = Date.now() - this.startTime;
108
+ const passed = this.results.filter((r) => r.status === "passed").length;
109
+ const failed = this.results.filter((r) => r.status === "failed").length;
110
+ const skipped = this.results.filter((r) => r.status === "skipped").length;
111
+ this.updateTestRun({
112
+ id: this.runId,
113
+ status: result.status === "passed" ? "completed" : "failed",
114
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
115
+ totalTests: this.results.length,
116
+ passedTests: passed,
117
+ failedTests: failed,
118
+ skippedTests: skipped,
119
+ duration
120
+ });
121
+ console.log("\u{1F504} Sending process end notification...");
122
+ await this.notifyProcessEnd({
123
+ runId: this.runId,
124
+ status: result.status === "passed" ? "completed" : "failed",
125
+ results: {
126
+ passed,
127
+ failed,
128
+ skipped,
129
+ duration
130
+ }
131
+ });
132
+ console.log(`
133
+ \u{1F4CA} Test run completed:`);
134
+ console.log(` \u2705 Passed: ${passed}`);
135
+ console.log(` \u274C Failed: ${failed}`);
136
+ console.log(` \u23ED\uFE0F Skipped: ${skipped}`);
137
+ console.log(` \u23F1\uFE0F Duration: ${(duration / 1e3).toFixed(1)}s`);
138
+ console.log(`
139
+ \u{1F310} View results: http://localhost:3000`);
140
+ }
141
+ generateStableTestId(test) {
142
+ const filePath = path.relative(process.cwd(), test.location.file);
143
+ const content = `${filePath}:${test.title}`;
144
+ let hash = 0;
145
+ for (let i = 0; i < content.length; i++) {
146
+ const char = content.charCodeAt(i);
147
+ hash = (hash << 5) - hash + char;
148
+ hash = hash & hash;
149
+ }
150
+ return `test-${Math.abs(hash).toString(36)}`;
151
+ }
152
+ mapStatus(status) {
153
+ switch (status) {
154
+ case "passed":
155
+ return "passed";
156
+ case "failed":
157
+ return "failed";
158
+ case "skipped":
159
+ return "skipped";
160
+ case "timedOut":
161
+ return "timedOut";
162
+ default:
163
+ return "failed";
164
+ }
165
+ }
166
+ processAttachments(attachments) {
167
+ return attachments.map((attachment) => ({
168
+ name: attachment.name,
169
+ path: attachment.path || "",
170
+ contentType: attachment.contentType
171
+ }));
172
+ }
173
+ createEnhancedErrorMessage(test, error) {
174
+ const originalStack = error.stack || error.message || "";
175
+ const stackMatch = originalStack.match(/at .*:(\d+):\d+/);
176
+ if (!stackMatch) {
177
+ return originalStack;
178
+ }
179
+ const lineNumber = parseInt(stackMatch[1]);
180
+ const filePath = test.location.file;
181
+ try {
182
+ const fileContent = fs.readFileSync(filePath, "utf-8");
183
+ const lines = fileContent.split("\n");
184
+ const contextLines = [];
185
+ const startLine = Math.max(0, lineNumber - 3);
186
+ const endLine = Math.min(lines.length - 1, lineNumber + 2);
187
+ for (let i = startLine; i <= endLine; i++) {
188
+ const lineNum = i + 1;
189
+ const isErrorLine = lineNum === lineNumber;
190
+ const prefix = isErrorLine ? ">" : " ";
191
+ const line = lines[i] || "";
192
+ contextLines.push(`${prefix} ${lineNum} |${line}`);
193
+ }
194
+ if (lineNumber <= lines.length) {
195
+ const errorLine = lines[lineNumber - 1] || "";
196
+ const caretPosition = this.findCaretPosition(errorLine, error.message);
197
+ if (caretPosition > 0) {
198
+ const spaces = " ".repeat(caretPosition + ` ${lineNumber} |`.length);
199
+ contextLines.splice(
200
+ contextLines.findIndex((line) => line.startsWith(">")) + 1,
201
+ 0,
202
+ ` |${spaces}^`
203
+ );
204
+ }
205
+ }
206
+ const mainErrorLines = originalStack.split("\n").filter(
207
+ (line) => !line.trim().startsWith("at ") || line.includes(path.relative(process.cwd(), filePath))
208
+ );
209
+ return [
210
+ ...mainErrorLines.slice(0, -1),
211
+ // Remove the last 'at' line
212
+ "",
213
+ ...contextLines,
214
+ "",
215
+ mainErrorLines[mainErrorLines.length - 1]
216
+ // Add back the 'at' line
217
+ ].join("\n");
218
+ } catch (err) {
219
+ console.log(err);
220
+ return originalStack;
221
+ }
222
+ }
223
+ findCaretPosition(line, errorMessage) {
224
+ if (errorMessage.includes("toBe")) {
225
+ const toBeIndex = line.indexOf("toBe");
226
+ if (toBeIndex !== -1) {
227
+ return toBeIndex + 2;
228
+ }
229
+ }
230
+ const expectIndex = line.indexOf("expect");
231
+ if (expectIndex !== -1) {
232
+ return expectIndex;
233
+ }
234
+ return 0;
235
+ }
236
+ getStatusIcon(status) {
237
+ switch (status) {
238
+ case "passed":
239
+ return "\u2705";
240
+ case "failed":
241
+ return "\u274C";
242
+ case "skipped":
243
+ return "\u23ED\uFE0F";
244
+ case "timedOut":
245
+ return "\u23F0";
246
+ default:
247
+ return "\u2753";
248
+ }
249
+ }
250
+ async sendTestResult(result) {
251
+ try {
252
+ const response = await fetch(`${this.apiBaseUrl}/api/tests`, {
253
+ method: "POST",
254
+ headers: {
255
+ "Content-Type": "application/json"
256
+ },
257
+ body: JSON.stringify(result)
258
+ });
259
+ if (!response.ok) {
260
+ console.warn(`\u26A0\uFE0F Failed to send test result: ${response.status}`);
261
+ const responseText = await response.text();
262
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
263
+ }
264
+ } catch (error) {
265
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
266
+ }
267
+ }
268
+ async createTestRun(run) {
269
+ try {
270
+ const response = await fetch(`${this.apiBaseUrl}/api/runs`, {
271
+ method: "POST",
272
+ headers: {
273
+ "Content-Type": "application/json"
274
+ },
275
+ body: JSON.stringify(run)
276
+ });
277
+ if (!response.ok) {
278
+ console.warn(`\u26A0\uFE0F Failed to create test run: ${response.status}`);
279
+ const responseText = await response.text();
280
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
281
+ }
282
+ } catch (error) {
283
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
284
+ }
285
+ }
286
+ async updateTestRun(run) {
287
+ try {
288
+ const response = await fetch(`${this.apiBaseUrl}/api/runs/${run.id}`, {
289
+ method: "PUT",
290
+ headers: {
291
+ "Content-Type": "application/json"
292
+ },
293
+ body: JSON.stringify(run)
294
+ });
295
+ if (!response.ok) {
296
+ console.warn(`\u26A0\uFE0F Failed to update test run: ${response.status}`);
297
+ const responseText = await response.text();
298
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
299
+ }
300
+ } catch (error) {
301
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
302
+ }
303
+ }
304
+ async notifyProcessStart(data) {
305
+ try {
306
+ console.log(`\u{1F4E4} Sending process start notification for: ${data.runId}`);
307
+ const response = await fetch(`${this.apiBaseUrl}/api/tests/process-start`, {
308
+ method: "POST",
309
+ headers: {
310
+ "Content-Type": "application/json"
311
+ },
312
+ body: JSON.stringify(data)
313
+ });
314
+ if (!response.ok) {
315
+ console.warn(`\u26A0\uFE0F Failed to notify process start: ${response.status}`);
316
+ const responseText = await response.text();
317
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
318
+ } else {
319
+ console.log(`\u2705 Process start notification sent successfully: ${data.runId}`);
320
+ }
321
+ } catch (error) {
322
+ console.warn(`\u26A0\uFE0F Process start notification failed: ${error}`);
323
+ }
324
+ }
325
+ async notifyProcessEnd(data) {
326
+ try {
327
+ console.log(`\u{1F4E4} Sending process end notification for: ${data.runId} (${data.status})`);
328
+ const response = await fetch(`${this.apiBaseUrl}/api/tests/process-end`, {
329
+ method: "POST",
330
+ headers: {
331
+ "Content-Type": "application/json"
332
+ },
333
+ body: JSON.stringify(data)
334
+ });
335
+ if (!response.ok) {
336
+ console.warn(`\u26A0\uFE0F Failed to notify process end: ${response.status}`);
337
+ const responseText = await response.text();
338
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
339
+ } else {
340
+ console.log(
341
+ `\u2705 Process end notification sent successfully: ${data.runId} (${data.status})`
342
+ );
343
+ }
344
+ } catch (error) {
345
+ console.warn(`\u26A0\uFE0F Process end notification failed: ${error}`);
346
+ }
347
+ }
348
+ setupCleanupHandlers() {
349
+ process.on("SIGINT", () => this.cleanup("interrupted"));
350
+ process.on("SIGTERM", () => this.cleanup("interrupted"));
351
+ process.on("uncaughtException", () => this.cleanup("interrupted"));
352
+ process.on("unhandledRejection", () => this.cleanup("interrupted"));
353
+ }
354
+ async cleanup(status = "interrupted") {
355
+ console.log("\u{1F9F9} Cleaning up reporter...");
356
+ try {
357
+ await this.notifyProcessEnd({
358
+ runId: this.runId,
359
+ status,
360
+ results: null
361
+ });
362
+ } catch (error) {
363
+ console.warn("\u26A0\uFE0F Cleanup notification failed:", error);
364
+ }
365
+ }
366
+ };
367
+ var index_default = YShvydakReporter;
368
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter'\n\nimport * as path from 'path'\nimport * as fs from 'fs'\nimport {v4 as uuidv4} from 'uuid'\nimport * as dotenv from 'dotenv'\ndotenv.config()\n\ninterface YShvydakTestResult {\n id: string\n testId: string\n runId: string\n name: string\n filePath: string\n status: 'passed' | 'failed' | 'skipped' | 'timedOut'\n duration: number\n timestamp: string\n errorMessage?: string\n errorStack?: string\n attachments: Array<{\n name: string\n path: string\n contentType: string\n }>\n}\n\ninterface YShvydakTestRun {\n id: string\n status: 'running' | 'completed' | 'failed'\n timestamp: string\n totalTests: number\n passedTests: number\n failedTests: number\n skippedTests: number\n duration: number\n}\n\ninterface ProcessStartData {\n runId: string\n type: 'run-all' | 'run-group' | 'rerun'\n totalTests?: number\n filePath?: string\n testId?: string\n originalTestId?: string\n}\n\ninterface ProcessEndData {\n runId: string\n status: 'completed' | 'failed' | 'interrupted'\n results?: {\n passed: number\n failed: number\n skipped: number\n duration: number\n } | null\n}\n\nclass YShvydakReporter implements Reporter {\n private runId: string = uuidv4()\n private results: YShvydakTestResult[] = []\n private startTime: number = 0\n private apiBaseUrl: string\n\n constructor() {\n // Get the base URL from environment variables\n let baseUrl = process.env.DASHBOARD_API_URL || 'http://localhost:3001'\n\n // Remove trailing /api if present (for backward compatibility)\n if (baseUrl.endsWith('/api')) {\n baseUrl = baseUrl.slice(0, -4)\n }\n\n this.apiBaseUrl = baseUrl\n\n console.log(`๐ŸŽญ YShvydak Dashboard Reporter initialized (Run ID: ${this.runId})`)\n console.log(`๐ŸŒ API Base URL: ${this.apiBaseUrl}`)\n\n if (!this.apiBaseUrl || this.apiBaseUrl === 'undefined') {\n console.warn(\n `โš ๏ธ Dashboard API URL not configured! Using fallback: http://localhost:3001`\n )\n this.apiBaseUrl = 'http://localhost:3001'\n }\n\n // Setup cleanup handlers for unexpected termination\n this.setupCleanupHandlers()\n }\n\n onBegin(_config: FullConfig, suite: Suite) {\n this.startTime = Date.now()\n\n // Notify dashboard that process is starting\n this.notifyProcessStart({\n runId: this.runId,\n type: 'run-all',\n totalTests: suite.allTests().length,\n })\n\n // Create test run\n this.createTestRun({\n id: this.runId,\n status: 'running',\n timestamp: new Date().toISOString(),\n totalTests: suite.allTests().length,\n passedTests: 0,\n failedTests: 0,\n skippedTests: 0,\n duration: 0,\n })\n\n console.log(`๐Ÿš€ Starting test run with ${suite.allTests().length} tests`)\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const testId = this.generateStableTestId(test)\n const filePath = path.relative(process.cwd(), test.location.file)\n\n // Create enhanced error message with code context like in original Playwright report\n let enhancedErrorMessage = result.error?.stack || result.error?.message\n if (result.status === 'failed' && result.error) {\n enhancedErrorMessage = this.createEnhancedErrorMessage(test, result.error)\n }\n\n const testResult: YShvydakTestResult = {\n id: uuidv4(),\n testId,\n runId: this.runId,\n name: test.title,\n filePath: filePath,\n status: this.mapStatus(result.status),\n duration: result.duration,\n timestamp: new Date().toISOString(),\n errorMessage: enhancedErrorMessage,\n errorStack: result.error?.stack,\n attachments: this.processAttachments(result.attachments),\n }\n\n this.results.push(testResult)\n\n // Send result to dashboard API\n this.sendTestResult(testResult)\n\n console.log(\n `${this.getStatusIcon(testResult.status)} ${testResult.name} (${testResult.duration}ms)`\n )\n }\n\n async onEnd(result: FullResult) {\n const duration = Date.now() - this.startTime\n const passed = this.results.filter((r) => r.status === 'passed').length\n const failed = this.results.filter((r) => r.status === 'failed').length\n const skipped = this.results.filter((r) => r.status === 'skipped').length\n\n // Update test run\n this.updateTestRun({\n id: this.runId,\n status: result.status === 'passed' ? 'completed' : 'failed',\n timestamp: new Date().toISOString(),\n totalTests: this.results.length,\n passedTests: passed,\n failedTests: failed,\n skippedTests: skipped,\n duration,\n })\n\n // Notify dashboard that process is ending\n console.log('๐Ÿ”„ Sending process end notification...')\n await this.notifyProcessEnd({\n runId: this.runId,\n status: result.status === 'passed' ? 'completed' : 'failed',\n results: {\n passed,\n failed,\n skipped,\n duration,\n },\n })\n\n console.log(`\\n๐Ÿ“Š Test run completed:`)\n console.log(` โœ… Passed: ${passed}`)\n console.log(` โŒ Failed: ${failed}`)\n console.log(` โญ๏ธ Skipped: ${skipped}`)\n console.log(` โฑ๏ธ Duration: ${(duration / 1000).toFixed(1)}s`)\n console.log(`\\n๐ŸŒ View results: http://localhost:3000`)\n }\n\n private generateStableTestId(test: TestCase): string {\n // Generate stable ID based on file path and test title\n const filePath = path.relative(process.cwd(), test.location.file)\n const content = `${filePath}:${test.title}`\n\n // Simple hash function for stable IDs\n let hash = 0\n for (let i = 0; i < content.length; i++) {\n const char = content.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n return `test-${Math.abs(hash).toString(36)}`\n }\n\n private mapStatus(status: string): 'passed' | 'failed' | 'skipped' | 'timedOut' {\n switch (status) {\n case 'passed':\n return 'passed'\n case 'failed':\n return 'failed'\n case 'skipped':\n return 'skipped'\n case 'timedOut':\n return 'timedOut'\n default:\n return 'failed'\n }\n }\n\n private processAttachments(attachments: TestResult['attachments']) {\n return attachments.map((attachment) => ({\n name: attachment.name,\n path: attachment.path || '',\n contentType: attachment.contentType,\n }))\n }\n\n private createEnhancedErrorMessage(test: TestCase, error: any): string {\n const originalStack = error.stack || error.message || ''\n\n // Extract line number from stack trace\n const stackMatch = originalStack.match(/at .*:(\\d+):\\d+/)\n if (!stackMatch) {\n return originalStack\n }\n\n const lineNumber = parseInt(stackMatch[1])\n const filePath = test.location.file\n\n try {\n // Read the actual file content\n const fileContent = fs.readFileSync(filePath, 'utf-8')\n const lines = fileContent.split('\\n')\n\n // Create context lines like in original Playwright report\n const contextLines: string[] = []\n const startLine = Math.max(0, lineNumber - 3)\n const endLine = Math.min(lines.length - 1, lineNumber + 2)\n\n for (let i = startLine; i <= endLine; i++) {\n const lineNum = i + 1\n const isErrorLine = lineNum === lineNumber\n const prefix = isErrorLine ? '>' : ' '\n const line = lines[i] || ''\n contextLines.push(`${prefix} ${lineNum} |${line}`)\n }\n\n // Add caret pointer for the error line\n if (lineNumber <= lines.length) {\n const errorLine = lines[lineNumber - 1] || ''\n const caretPosition = this.findCaretPosition(errorLine, error.message)\n if (caretPosition > 0) {\n const spaces = ' '.repeat(caretPosition + ` ${lineNumber} |`.length)\n contextLines.splice(\n contextLines.findIndex((line) => line.startsWith('>')) + 1,\n 0,\n ` |${spaces}^`\n )\n }\n }\n\n // Combine original error message with code context\n const mainErrorLines = originalStack\n .split('\\n')\n .filter(\n (line: any) =>\n !line.trim().startsWith('at ') ||\n line.includes(path.relative(process.cwd(), filePath))\n )\n\n return [\n ...mainErrorLines.slice(0, -1), // Remove the last 'at' line\n '',\n ...contextLines,\n '',\n mainErrorLines[mainErrorLines.length - 1], // Add back the 'at' line\n ].join('\\n')\n } catch (err) {\n console.log(err)\n return originalStack\n }\n }\n\n private findCaretPosition(line: string, errorMessage: string): number {\n // Try to find the position of the error in the line\n // This is a simple heuristic - in real Playwright it's more sophisticated\n if (errorMessage.includes('toBe')) {\n const toBeIndex = line.indexOf('toBe')\n if (toBeIndex !== -1) {\n return toBeIndex + 2 // Position at 'Be'\n }\n }\n\n // Default to finding 'expect' if present\n const expectIndex = line.indexOf('expect')\n if (expectIndex !== -1) {\n return expectIndex\n }\n\n return 0\n }\n\n private getStatusIcon(status: string): string {\n switch (status) {\n case 'passed':\n return 'โœ…'\n case 'failed':\n return 'โŒ'\n case 'skipped':\n return 'โญ๏ธ'\n case 'timedOut':\n return 'โฐ'\n default:\n return 'โ“'\n }\n }\n\n private async sendTestResult(result: YShvydakTestResult) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/tests`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(result),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to send test result: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async createTestRun(run: YShvydakTestRun) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/runs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(run),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to create test run: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async updateTestRun(run: YShvydakTestRun) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/runs/${run.id}`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(run),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to update test run: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async notifyProcessStart(data: ProcessStartData) {\n try {\n console.log(`๐Ÿ“ค Sending process start notification for: ${data.runId}`)\n const response = await fetch(`${this.apiBaseUrl}/api/tests/process-start`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to notify process start: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n } else {\n console.log(`โœ… Process start notification sent successfully: ${data.runId}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Process start notification failed: ${error}`)\n }\n }\n\n private async notifyProcessEnd(data: ProcessEndData) {\n try {\n console.log(`๐Ÿ“ค Sending process end notification for: ${data.runId} (${data.status})`)\n const response = await fetch(`${this.apiBaseUrl}/api/tests/process-end`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to notify process end: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n } else {\n console.log(\n `โœ… Process end notification sent successfully: ${data.runId} (${data.status})`\n )\n }\n } catch (error) {\n console.warn(`โš ๏ธ Process end notification failed: ${error}`)\n }\n }\n\n private setupCleanupHandlers() {\n // Handle process termination signals\n process.on('SIGINT', () => this.cleanup('interrupted'))\n process.on('SIGTERM', () => this.cleanup('interrupted'))\n process.on('uncaughtException', () => this.cleanup('interrupted'))\n process.on('unhandledRejection', () => this.cleanup('interrupted'))\n }\n\n private async cleanup(status: 'interrupted' = 'interrupted') {\n console.log('๐Ÿงน Cleaning up reporter...')\n try {\n await this.notifyProcessEnd({\n runId: this.runId,\n status: status,\n results: null,\n })\n } catch (error) {\n console.warn('โš ๏ธ Cleanup notification failed:', error)\n }\n }\n}\n\nexport default YShvydakReporter\n"],"mappings":"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","names":["uuidv4"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,337 @@
1
+ // src/index.ts
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import * as dotenv from "dotenv";
6
+ dotenv.config();
7
+ var YShvydakReporter = class {
8
+ constructor() {
9
+ this.runId = uuidv4();
10
+ this.results = [];
11
+ this.startTime = 0;
12
+ let baseUrl = process.env.DASHBOARD_API_URL || "http://localhost:3001";
13
+ if (baseUrl.endsWith("/api")) {
14
+ baseUrl = baseUrl.slice(0, -4);
15
+ }
16
+ this.apiBaseUrl = baseUrl;
17
+ console.log(`\u{1F3AD} YShvydak Dashboard Reporter initialized (Run ID: ${this.runId})`);
18
+ console.log(`\u{1F310} API Base URL: ${this.apiBaseUrl}`);
19
+ if (!this.apiBaseUrl || this.apiBaseUrl === "undefined") {
20
+ console.warn(
21
+ `\u26A0\uFE0F Dashboard API URL not configured! Using fallback: http://localhost:3001`
22
+ );
23
+ this.apiBaseUrl = "http://localhost:3001";
24
+ }
25
+ this.setupCleanupHandlers();
26
+ }
27
+ onBegin(_config, suite) {
28
+ this.startTime = Date.now();
29
+ this.notifyProcessStart({
30
+ runId: this.runId,
31
+ type: "run-all",
32
+ totalTests: suite.allTests().length
33
+ });
34
+ this.createTestRun({
35
+ id: this.runId,
36
+ status: "running",
37
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
38
+ totalTests: suite.allTests().length,
39
+ passedTests: 0,
40
+ failedTests: 0,
41
+ skippedTests: 0,
42
+ duration: 0
43
+ });
44
+ console.log(`\u{1F680} Starting test run with ${suite.allTests().length} tests`);
45
+ }
46
+ onTestEnd(test, result) {
47
+ const testId = this.generateStableTestId(test);
48
+ const filePath = path.relative(process.cwd(), test.location.file);
49
+ let enhancedErrorMessage = result.error?.stack || result.error?.message;
50
+ if (result.status === "failed" && result.error) {
51
+ enhancedErrorMessage = this.createEnhancedErrorMessage(test, result.error);
52
+ }
53
+ const testResult = {
54
+ id: uuidv4(),
55
+ testId,
56
+ runId: this.runId,
57
+ name: test.title,
58
+ filePath,
59
+ status: this.mapStatus(result.status),
60
+ duration: result.duration,
61
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
62
+ errorMessage: enhancedErrorMessage,
63
+ errorStack: result.error?.stack,
64
+ attachments: this.processAttachments(result.attachments)
65
+ };
66
+ this.results.push(testResult);
67
+ this.sendTestResult(testResult);
68
+ console.log(
69
+ `${this.getStatusIcon(testResult.status)} ${testResult.name} (${testResult.duration}ms)`
70
+ );
71
+ }
72
+ async onEnd(result) {
73
+ const duration = Date.now() - this.startTime;
74
+ const passed = this.results.filter((r) => r.status === "passed").length;
75
+ const failed = this.results.filter((r) => r.status === "failed").length;
76
+ const skipped = this.results.filter((r) => r.status === "skipped").length;
77
+ this.updateTestRun({
78
+ id: this.runId,
79
+ status: result.status === "passed" ? "completed" : "failed",
80
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
81
+ totalTests: this.results.length,
82
+ passedTests: passed,
83
+ failedTests: failed,
84
+ skippedTests: skipped,
85
+ duration
86
+ });
87
+ console.log("\u{1F504} Sending process end notification...");
88
+ await this.notifyProcessEnd({
89
+ runId: this.runId,
90
+ status: result.status === "passed" ? "completed" : "failed",
91
+ results: {
92
+ passed,
93
+ failed,
94
+ skipped,
95
+ duration
96
+ }
97
+ });
98
+ console.log(`
99
+ \u{1F4CA} Test run completed:`);
100
+ console.log(` \u2705 Passed: ${passed}`);
101
+ console.log(` \u274C Failed: ${failed}`);
102
+ console.log(` \u23ED\uFE0F Skipped: ${skipped}`);
103
+ console.log(` \u23F1\uFE0F Duration: ${(duration / 1e3).toFixed(1)}s`);
104
+ console.log(`
105
+ \u{1F310} View results: http://localhost:3000`);
106
+ }
107
+ generateStableTestId(test) {
108
+ const filePath = path.relative(process.cwd(), test.location.file);
109
+ const content = `${filePath}:${test.title}`;
110
+ let hash = 0;
111
+ for (let i = 0; i < content.length; i++) {
112
+ const char = content.charCodeAt(i);
113
+ hash = (hash << 5) - hash + char;
114
+ hash = hash & hash;
115
+ }
116
+ return `test-${Math.abs(hash).toString(36)}`;
117
+ }
118
+ mapStatus(status) {
119
+ switch (status) {
120
+ case "passed":
121
+ return "passed";
122
+ case "failed":
123
+ return "failed";
124
+ case "skipped":
125
+ return "skipped";
126
+ case "timedOut":
127
+ return "timedOut";
128
+ default:
129
+ return "failed";
130
+ }
131
+ }
132
+ processAttachments(attachments) {
133
+ return attachments.map((attachment) => ({
134
+ name: attachment.name,
135
+ path: attachment.path || "",
136
+ contentType: attachment.contentType
137
+ }));
138
+ }
139
+ createEnhancedErrorMessage(test, error) {
140
+ const originalStack = error.stack || error.message || "";
141
+ const stackMatch = originalStack.match(/at .*:(\d+):\d+/);
142
+ if (!stackMatch) {
143
+ return originalStack;
144
+ }
145
+ const lineNumber = parseInt(stackMatch[1]);
146
+ const filePath = test.location.file;
147
+ try {
148
+ const fileContent = fs.readFileSync(filePath, "utf-8");
149
+ const lines = fileContent.split("\n");
150
+ const contextLines = [];
151
+ const startLine = Math.max(0, lineNumber - 3);
152
+ const endLine = Math.min(lines.length - 1, lineNumber + 2);
153
+ for (let i = startLine; i <= endLine; i++) {
154
+ const lineNum = i + 1;
155
+ const isErrorLine = lineNum === lineNumber;
156
+ const prefix = isErrorLine ? ">" : " ";
157
+ const line = lines[i] || "";
158
+ contextLines.push(`${prefix} ${lineNum} |${line}`);
159
+ }
160
+ if (lineNumber <= lines.length) {
161
+ const errorLine = lines[lineNumber - 1] || "";
162
+ const caretPosition = this.findCaretPosition(errorLine, error.message);
163
+ if (caretPosition > 0) {
164
+ const spaces = " ".repeat(caretPosition + ` ${lineNumber} |`.length);
165
+ contextLines.splice(
166
+ contextLines.findIndex((line) => line.startsWith(">")) + 1,
167
+ 0,
168
+ ` |${spaces}^`
169
+ );
170
+ }
171
+ }
172
+ const mainErrorLines = originalStack.split("\n").filter(
173
+ (line) => !line.trim().startsWith("at ") || line.includes(path.relative(process.cwd(), filePath))
174
+ );
175
+ return [
176
+ ...mainErrorLines.slice(0, -1),
177
+ // Remove the last 'at' line
178
+ "",
179
+ ...contextLines,
180
+ "",
181
+ mainErrorLines[mainErrorLines.length - 1]
182
+ // Add back the 'at' line
183
+ ].join("\n");
184
+ } catch (err) {
185
+ console.log(err);
186
+ return originalStack;
187
+ }
188
+ }
189
+ findCaretPosition(line, errorMessage) {
190
+ if (errorMessage.includes("toBe")) {
191
+ const toBeIndex = line.indexOf("toBe");
192
+ if (toBeIndex !== -1) {
193
+ return toBeIndex + 2;
194
+ }
195
+ }
196
+ const expectIndex = line.indexOf("expect");
197
+ if (expectIndex !== -1) {
198
+ return expectIndex;
199
+ }
200
+ return 0;
201
+ }
202
+ getStatusIcon(status) {
203
+ switch (status) {
204
+ case "passed":
205
+ return "\u2705";
206
+ case "failed":
207
+ return "\u274C";
208
+ case "skipped":
209
+ return "\u23ED\uFE0F";
210
+ case "timedOut":
211
+ return "\u23F0";
212
+ default:
213
+ return "\u2753";
214
+ }
215
+ }
216
+ async sendTestResult(result) {
217
+ try {
218
+ const response = await fetch(`${this.apiBaseUrl}/api/tests`, {
219
+ method: "POST",
220
+ headers: {
221
+ "Content-Type": "application/json"
222
+ },
223
+ body: JSON.stringify(result)
224
+ });
225
+ if (!response.ok) {
226
+ console.warn(`\u26A0\uFE0F Failed to send test result: ${response.status}`);
227
+ const responseText = await response.text();
228
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
229
+ }
230
+ } catch (error) {
231
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
232
+ }
233
+ }
234
+ async createTestRun(run) {
235
+ try {
236
+ const response = await fetch(`${this.apiBaseUrl}/api/runs`, {
237
+ method: "POST",
238
+ headers: {
239
+ "Content-Type": "application/json"
240
+ },
241
+ body: JSON.stringify(run)
242
+ });
243
+ if (!response.ok) {
244
+ console.warn(`\u26A0\uFE0F Failed to create test run: ${response.status}`);
245
+ const responseText = await response.text();
246
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
247
+ }
248
+ } catch (error) {
249
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
250
+ }
251
+ }
252
+ async updateTestRun(run) {
253
+ try {
254
+ const response = await fetch(`${this.apiBaseUrl}/api/runs/${run.id}`, {
255
+ method: "PUT",
256
+ headers: {
257
+ "Content-Type": "application/json"
258
+ },
259
+ body: JSON.stringify(run)
260
+ });
261
+ if (!response.ok) {
262
+ console.warn(`\u26A0\uFE0F Failed to update test run: ${response.status}`);
263
+ const responseText = await response.text();
264
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
265
+ }
266
+ } catch (error) {
267
+ console.warn(`\u26A0\uFE0F Dashboard API not available: ${error}`);
268
+ }
269
+ }
270
+ async notifyProcessStart(data) {
271
+ try {
272
+ console.log(`\u{1F4E4} Sending process start notification for: ${data.runId}`);
273
+ const response = await fetch(`${this.apiBaseUrl}/api/tests/process-start`, {
274
+ method: "POST",
275
+ headers: {
276
+ "Content-Type": "application/json"
277
+ },
278
+ body: JSON.stringify(data)
279
+ });
280
+ if (!response.ok) {
281
+ console.warn(`\u26A0\uFE0F Failed to notify process start: ${response.status}`);
282
+ const responseText = await response.text();
283
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
284
+ } else {
285
+ console.log(`\u2705 Process start notification sent successfully: ${data.runId}`);
286
+ }
287
+ } catch (error) {
288
+ console.warn(`\u26A0\uFE0F Process start notification failed: ${error}`);
289
+ }
290
+ }
291
+ async notifyProcessEnd(data) {
292
+ try {
293
+ console.log(`\u{1F4E4} Sending process end notification for: ${data.runId} (${data.status})`);
294
+ const response = await fetch(`${this.apiBaseUrl}/api/tests/process-end`, {
295
+ method: "POST",
296
+ headers: {
297
+ "Content-Type": "application/json"
298
+ },
299
+ body: JSON.stringify(data)
300
+ });
301
+ if (!response.ok) {
302
+ console.warn(`\u26A0\uFE0F Failed to notify process end: ${response.status}`);
303
+ const responseText = await response.text();
304
+ console.warn(`\u26A0\uFE0F Response: ${responseText}`);
305
+ } else {
306
+ console.log(
307
+ `\u2705 Process end notification sent successfully: ${data.runId} (${data.status})`
308
+ );
309
+ }
310
+ } catch (error) {
311
+ console.warn(`\u26A0\uFE0F Process end notification failed: ${error}`);
312
+ }
313
+ }
314
+ setupCleanupHandlers() {
315
+ process.on("SIGINT", () => this.cleanup("interrupted"));
316
+ process.on("SIGTERM", () => this.cleanup("interrupted"));
317
+ process.on("uncaughtException", () => this.cleanup("interrupted"));
318
+ process.on("unhandledRejection", () => this.cleanup("interrupted"));
319
+ }
320
+ async cleanup(status = "interrupted") {
321
+ console.log("\u{1F9F9} Cleaning up reporter...");
322
+ try {
323
+ await this.notifyProcessEnd({
324
+ runId: this.runId,
325
+ status,
326
+ results: null
327
+ });
328
+ } catch (error) {
329
+ console.warn("\u26A0\uFE0F Cleanup notification failed:", error);
330
+ }
331
+ }
332
+ };
333
+ var index_default = YShvydakReporter;
334
+ export {
335
+ index_default as default
336
+ };
337
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter'\n\nimport * as path from 'path'\nimport * as fs from 'fs'\nimport {v4 as uuidv4} from 'uuid'\nimport * as dotenv from 'dotenv'\ndotenv.config()\n\ninterface YShvydakTestResult {\n id: string\n testId: string\n runId: string\n name: string\n filePath: string\n status: 'passed' | 'failed' | 'skipped' | 'timedOut'\n duration: number\n timestamp: string\n errorMessage?: string\n errorStack?: string\n attachments: Array<{\n name: string\n path: string\n contentType: string\n }>\n}\n\ninterface YShvydakTestRun {\n id: string\n status: 'running' | 'completed' | 'failed'\n timestamp: string\n totalTests: number\n passedTests: number\n failedTests: number\n skippedTests: number\n duration: number\n}\n\ninterface ProcessStartData {\n runId: string\n type: 'run-all' | 'run-group' | 'rerun'\n totalTests?: number\n filePath?: string\n testId?: string\n originalTestId?: string\n}\n\ninterface ProcessEndData {\n runId: string\n status: 'completed' | 'failed' | 'interrupted'\n results?: {\n passed: number\n failed: number\n skipped: number\n duration: number\n } | null\n}\n\nclass YShvydakReporter implements Reporter {\n private runId: string = uuidv4()\n private results: YShvydakTestResult[] = []\n private startTime: number = 0\n private apiBaseUrl: string\n\n constructor() {\n // Get the base URL from environment variables\n let baseUrl = process.env.DASHBOARD_API_URL || 'http://localhost:3001'\n\n // Remove trailing /api if present (for backward compatibility)\n if (baseUrl.endsWith('/api')) {\n baseUrl = baseUrl.slice(0, -4)\n }\n\n this.apiBaseUrl = baseUrl\n\n console.log(`๐ŸŽญ YShvydak Dashboard Reporter initialized (Run ID: ${this.runId})`)\n console.log(`๐ŸŒ API Base URL: ${this.apiBaseUrl}`)\n\n if (!this.apiBaseUrl || this.apiBaseUrl === 'undefined') {\n console.warn(\n `โš ๏ธ Dashboard API URL not configured! Using fallback: http://localhost:3001`\n )\n this.apiBaseUrl = 'http://localhost:3001'\n }\n\n // Setup cleanup handlers for unexpected termination\n this.setupCleanupHandlers()\n }\n\n onBegin(_config: FullConfig, suite: Suite) {\n this.startTime = Date.now()\n\n // Notify dashboard that process is starting\n this.notifyProcessStart({\n runId: this.runId,\n type: 'run-all',\n totalTests: suite.allTests().length,\n })\n\n // Create test run\n this.createTestRun({\n id: this.runId,\n status: 'running',\n timestamp: new Date().toISOString(),\n totalTests: suite.allTests().length,\n passedTests: 0,\n failedTests: 0,\n skippedTests: 0,\n duration: 0,\n })\n\n console.log(`๐Ÿš€ Starting test run with ${suite.allTests().length} tests`)\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const testId = this.generateStableTestId(test)\n const filePath = path.relative(process.cwd(), test.location.file)\n\n // Create enhanced error message with code context like in original Playwright report\n let enhancedErrorMessage = result.error?.stack || result.error?.message\n if (result.status === 'failed' && result.error) {\n enhancedErrorMessage = this.createEnhancedErrorMessage(test, result.error)\n }\n\n const testResult: YShvydakTestResult = {\n id: uuidv4(),\n testId,\n runId: this.runId,\n name: test.title,\n filePath: filePath,\n status: this.mapStatus(result.status),\n duration: result.duration,\n timestamp: new Date().toISOString(),\n errorMessage: enhancedErrorMessage,\n errorStack: result.error?.stack,\n attachments: this.processAttachments(result.attachments),\n }\n\n this.results.push(testResult)\n\n // Send result to dashboard API\n this.sendTestResult(testResult)\n\n console.log(\n `${this.getStatusIcon(testResult.status)} ${testResult.name} (${testResult.duration}ms)`\n )\n }\n\n async onEnd(result: FullResult) {\n const duration = Date.now() - this.startTime\n const passed = this.results.filter((r) => r.status === 'passed').length\n const failed = this.results.filter((r) => r.status === 'failed').length\n const skipped = this.results.filter((r) => r.status === 'skipped').length\n\n // Update test run\n this.updateTestRun({\n id: this.runId,\n status: result.status === 'passed' ? 'completed' : 'failed',\n timestamp: new Date().toISOString(),\n totalTests: this.results.length,\n passedTests: passed,\n failedTests: failed,\n skippedTests: skipped,\n duration,\n })\n\n // Notify dashboard that process is ending\n console.log('๐Ÿ”„ Sending process end notification...')\n await this.notifyProcessEnd({\n runId: this.runId,\n status: result.status === 'passed' ? 'completed' : 'failed',\n results: {\n passed,\n failed,\n skipped,\n duration,\n },\n })\n\n console.log(`\\n๐Ÿ“Š Test run completed:`)\n console.log(` โœ… Passed: ${passed}`)\n console.log(` โŒ Failed: ${failed}`)\n console.log(` โญ๏ธ Skipped: ${skipped}`)\n console.log(` โฑ๏ธ Duration: ${(duration / 1000).toFixed(1)}s`)\n console.log(`\\n๐ŸŒ View results: http://localhost:3000`)\n }\n\n private generateStableTestId(test: TestCase): string {\n // Generate stable ID based on file path and test title\n const filePath = path.relative(process.cwd(), test.location.file)\n const content = `${filePath}:${test.title}`\n\n // Simple hash function for stable IDs\n let hash = 0\n for (let i = 0; i < content.length; i++) {\n const char = content.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n return `test-${Math.abs(hash).toString(36)}`\n }\n\n private mapStatus(status: string): 'passed' | 'failed' | 'skipped' | 'timedOut' {\n switch (status) {\n case 'passed':\n return 'passed'\n case 'failed':\n return 'failed'\n case 'skipped':\n return 'skipped'\n case 'timedOut':\n return 'timedOut'\n default:\n return 'failed'\n }\n }\n\n private processAttachments(attachments: TestResult['attachments']) {\n return attachments.map((attachment) => ({\n name: attachment.name,\n path: attachment.path || '',\n contentType: attachment.contentType,\n }))\n }\n\n private createEnhancedErrorMessage(test: TestCase, error: any): string {\n const originalStack = error.stack || error.message || ''\n\n // Extract line number from stack trace\n const stackMatch = originalStack.match(/at .*:(\\d+):\\d+/)\n if (!stackMatch) {\n return originalStack\n }\n\n const lineNumber = parseInt(stackMatch[1])\n const filePath = test.location.file\n\n try {\n // Read the actual file content\n const fileContent = fs.readFileSync(filePath, 'utf-8')\n const lines = fileContent.split('\\n')\n\n // Create context lines like in original Playwright report\n const contextLines: string[] = []\n const startLine = Math.max(0, lineNumber - 3)\n const endLine = Math.min(lines.length - 1, lineNumber + 2)\n\n for (let i = startLine; i <= endLine; i++) {\n const lineNum = i + 1\n const isErrorLine = lineNum === lineNumber\n const prefix = isErrorLine ? '>' : ' '\n const line = lines[i] || ''\n contextLines.push(`${prefix} ${lineNum} |${line}`)\n }\n\n // Add caret pointer for the error line\n if (lineNumber <= lines.length) {\n const errorLine = lines[lineNumber - 1] || ''\n const caretPosition = this.findCaretPosition(errorLine, error.message)\n if (caretPosition > 0) {\n const spaces = ' '.repeat(caretPosition + ` ${lineNumber} |`.length)\n contextLines.splice(\n contextLines.findIndex((line) => line.startsWith('>')) + 1,\n 0,\n ` |${spaces}^`\n )\n }\n }\n\n // Combine original error message with code context\n const mainErrorLines = originalStack\n .split('\\n')\n .filter(\n (line: any) =>\n !line.trim().startsWith('at ') ||\n line.includes(path.relative(process.cwd(), filePath))\n )\n\n return [\n ...mainErrorLines.slice(0, -1), // Remove the last 'at' line\n '',\n ...contextLines,\n '',\n mainErrorLines[mainErrorLines.length - 1], // Add back the 'at' line\n ].join('\\n')\n } catch (err) {\n console.log(err)\n return originalStack\n }\n }\n\n private findCaretPosition(line: string, errorMessage: string): number {\n // Try to find the position of the error in the line\n // This is a simple heuristic - in real Playwright it's more sophisticated\n if (errorMessage.includes('toBe')) {\n const toBeIndex = line.indexOf('toBe')\n if (toBeIndex !== -1) {\n return toBeIndex + 2 // Position at 'Be'\n }\n }\n\n // Default to finding 'expect' if present\n const expectIndex = line.indexOf('expect')\n if (expectIndex !== -1) {\n return expectIndex\n }\n\n return 0\n }\n\n private getStatusIcon(status: string): string {\n switch (status) {\n case 'passed':\n return 'โœ…'\n case 'failed':\n return 'โŒ'\n case 'skipped':\n return 'โญ๏ธ'\n case 'timedOut':\n return 'โฐ'\n default:\n return 'โ“'\n }\n }\n\n private async sendTestResult(result: YShvydakTestResult) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/tests`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(result),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to send test result: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async createTestRun(run: YShvydakTestRun) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/runs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(run),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to create test run: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async updateTestRun(run: YShvydakTestRun) {\n try {\n const response = await fetch(`${this.apiBaseUrl}/api/runs/${run.id}`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(run),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to update test run: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Dashboard API not available: ${error}`)\n }\n }\n\n private async notifyProcessStart(data: ProcessStartData) {\n try {\n console.log(`๐Ÿ“ค Sending process start notification for: ${data.runId}`)\n const response = await fetch(`${this.apiBaseUrl}/api/tests/process-start`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to notify process start: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n } else {\n console.log(`โœ… Process start notification sent successfully: ${data.runId}`)\n }\n } catch (error) {\n console.warn(`โš ๏ธ Process start notification failed: ${error}`)\n }\n }\n\n private async notifyProcessEnd(data: ProcessEndData) {\n try {\n console.log(`๐Ÿ“ค Sending process end notification for: ${data.runId} (${data.status})`)\n const response = await fetch(`${this.apiBaseUrl}/api/tests/process-end`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data),\n })\n\n if (!response.ok) {\n console.warn(`โš ๏ธ Failed to notify process end: ${response.status}`)\n const responseText = await response.text()\n console.warn(`โš ๏ธ Response: ${responseText}`)\n } else {\n console.log(\n `โœ… Process end notification sent successfully: ${data.runId} (${data.status})`\n )\n }\n } catch (error) {\n console.warn(`โš ๏ธ Process end notification failed: ${error}`)\n }\n }\n\n private setupCleanupHandlers() {\n // Handle process termination signals\n process.on('SIGINT', () => this.cleanup('interrupted'))\n process.on('SIGTERM', () => this.cleanup('interrupted'))\n process.on('uncaughtException', () => this.cleanup('interrupted'))\n process.on('unhandledRejection', () => this.cleanup('interrupted'))\n }\n\n private async cleanup(status: 'interrupted' = 'interrupted') {\n console.log('๐Ÿงน Cleaning up reporter...')\n try {\n await this.notifyProcessEnd({\n runId: this.runId,\n status: status,\n results: null,\n })\n } catch (error) {\n console.warn('โš ๏ธ Cleanup notification failed:', error)\n }\n }\n}\n\nexport default YShvydakReporter\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "playwright-dashboard-reporter",
3
+ "version": "1.0.0",
4
+ "description": "Real-time Playwright test dashboard with rerun capabilities and comprehensive test reporting",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/shvydak/yshvydak-test-dashboard.git",
18
+ "directory": "packages/reporter"
19
+ },
20
+ "homepage": "https://github.com/shvydak/yshvydak-test-dashboard#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/shvydak/yshvydak-test-dashboard/issues"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
26
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
27
+ "type-check": "tsc --noEmit",
28
+ "prepublishOnly": "npm run build && npm run type-check"
29
+ },
30
+ "keywords": [
31
+ "playwright",
32
+ "playwright-reporter",
33
+ "test-reporter",
34
+ "testing",
35
+ "dashboard",
36
+ "test-automation",
37
+ "qa",
38
+ "test-results",
39
+ "real-time-dashboard",
40
+ "test-monitoring"
41
+ ],
42
+ "author": "YShvydak",
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=18.0.0",
46
+ "npm": ">=10.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "dependencies": {
52
+ "uuid": "^11.1.0",
53
+ "dotenv": "^16.3.1"
54
+ },
55
+ "peerDependencies": {
56
+ "@playwright/test": "^1.40.0"
57
+ },
58
+ "devDependencies": {
59
+ "@playwright/test": "^1.55.0",
60
+ "@types/node": "^22.5.4",
61
+ "@types/uuid": "^10.0.0",
62
+ "tsup": "^8.3.5",
63
+ "typescript": "^5.9.2"
64
+ },
65
+ "files": [
66
+ "dist"
67
+ ]
68
+ }