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 +259 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +368 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +337 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
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**
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|