@arghajit/playwright-pulse-report 0.3.2 → 0.3.4
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/LICENSE +21 -0
- package/README.md +98 -88
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +85 -25
- package/dist/types/index.d.ts +8 -2
- package/dist/utils/compression-utils.d.ts +19 -0
- package/dist/utils/compression-utils.js +112 -0
- package/package.json +12 -6
- package/scripts/generate-email-report.mjs +42 -12
- package/scripts/generate-report.mjs +2367 -661
- package/scripts/generate-static-report.mjs +4566 -1065
- package/scripts/generate-trend.mjs +0 -0
- package/scripts/merge-pulse-report.js +160 -36
- package/scripts/sendReport.mjs +153 -65
- package/scripts/terminal-logo.mjs +51 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -26
- package/dist/playwright-pulse-reporter.d.ts +0 -26
- package/dist/playwright-pulse-reporter.js +0 -304
- package/dist/reporter/lib/report-types.d.ts +0 -8
- package/dist/reporter/lib/report-types.js +0 -2
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +0 -1
- package/dist/reporter/reporter/playwright-pulse-reporter.js +0 -398
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +0 -1
- package/dist/reporter/types/index.d.ts +0 -52
- package/dist/reporter/types/index.js +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arghajit Singha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
_The ultimate Playwright reporter — Interactive dashboard with historical trend analytics, CI/CD-ready standalone HTML reports, and sharding support for scalable test execution._
|
|
10
10
|
|
|
11
|
-
## [Live Demo](https://
|
|
11
|
+
## [Live Demo](https://arghajit47.github.io/playwright-pulse/demo.html)
|
|
12
12
|
|
|
13
|
-
## 
|
|
13
|
+
## 
|
|
14
14
|
|
|
15
|
-
## **Documentation**: [Pulse Report](https://playwright-pulse
|
|
15
|
+
## **Documentation**: [Pulse Report](https://arghajit47.github.io/playwright-pulse/)
|
|
16
16
|
|
|
17
17
|
## Available Scripts
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ The project provides these utility commands:
|
|
|
21
21
|
| Command | Description |
|
|
22
22
|
|------------------------|-----------------------------------------------------------------------------|
|
|
23
23
|
| `generate-report` | Generates playwright-pulse-report.html, Loads screenshots and images dynamically from the attachments/ directory, Produces a lighter HTML file with faster initial load, Requires attachments/ directory to be present when viewing the report |
|
|
24
|
-
| `generate-pulse-report`| Generates `playwright-pulse-static-report.html`, Self-contained, no server required, Preserves all dashboard functionality, all the attachments are
|
|
24
|
+
| `generate-pulse-report`| Generates `playwright-pulse-static-report.html`, Self-contained, no server required, Preserves all dashboard functionality, all the attachments are embedded in the report, no need to have attachments/ directory when viewing the report, with a dark theme and better initial load handling |
|
|
25
25
|
| `merge-pulse-report` | Combines multiple parallel test json reports, basically used in sharding |
|
|
26
26
|
| `generate-trend` | Analyzes historical trends in test results |
|
|
27
27
|
| `generate-email-report`| Generates email-friendly report versions |
|
|
@@ -101,24 +101,7 @@ npx merge-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
|
|
|
101
101
|
|
|
102
102
|
**Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
import { defineConfig } from "@playwright/test";
|
|
106
|
-
import * as path from "path";
|
|
107
|
-
|
|
108
|
-
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "{YOUR_CUSTOM_REPORT_FOLDER}");
|
|
109
|
-
|
|
110
|
-
export default defineConfig({
|
|
111
|
-
reporter: [
|
|
112
|
-
["list"],
|
|
113
|
-
[
|
|
114
|
-
"@arghajit/playwright-pulse-report",
|
|
115
|
-
{
|
|
116
|
-
outputDir: CUSTOM_REPORT_DIR, // Must match CLI --outputDir
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
],
|
|
120
|
-
});
|
|
121
|
-
```
|
|
104
|
+

|
|
122
105
|
|
|
123
106
|
## 📊 Report Options
|
|
124
107
|
|
|
@@ -240,76 +223,102 @@ Under the hood, this will:
|
|
|
240
223
|
### Basic Workflow
|
|
241
224
|
|
|
242
225
|
```yaml
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
- name:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- name:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
226
|
+
# .github/workflows/playwright.yml
|
|
227
|
+
name: Playwright Tests
|
|
228
|
+
on:
|
|
229
|
+
push:
|
|
230
|
+
branches: [ main, master ]
|
|
231
|
+
pull_request:
|
|
232
|
+
branches: [ main, master ]
|
|
233
|
+
jobs:
|
|
234
|
+
test:
|
|
235
|
+
timeout-minutes: 60
|
|
236
|
+
runs-on: ubuntu-latest
|
|
237
|
+
steps:
|
|
238
|
+
- uses: actions/checkout@v4
|
|
239
|
+
- uses: actions/setup-node@v4
|
|
240
|
+
with:
|
|
241
|
+
node-version: lts/*
|
|
242
|
+
- name: Install dependencies
|
|
243
|
+
run: npm ci
|
|
244
|
+
- name: Install Playwright Browsers
|
|
245
|
+
run: npx playwright install --with-deps
|
|
246
|
+
- name: Run Playwright tests
|
|
247
|
+
run: npm run test
|
|
248
|
+
- name: Generate Pulse Report
|
|
249
|
+
run: npx generate-report
|
|
250
|
+
- uses: actions/upload-artifact@v4
|
|
251
|
+
if: always()
|
|
252
|
+
with:
|
|
253
|
+
name: pulse-report
|
|
254
|
+
path: pulse-report/
|
|
255
|
+
retention-days: 30
|
|
269
256
|
```
|
|
270
257
|
|
|
258
|
+
For more details, please refer to the [Pulse Report Basic CI/CD Integration](https://arghajit47.github.io/playwright-pulse/advanced-usage.html).
|
|
259
|
+
|
|
271
260
|
### Sharded Workflow
|
|
272
261
|
|
|
273
262
|
```yaml
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
263
|
+
# .github/workflows/playwright.yml
|
|
264
|
+
name: Playwright Tests with Pulse Report
|
|
265
|
+
on: [push]
|
|
266
|
+
jobs:
|
|
267
|
+
test:
|
|
268
|
+
timeout-minutes: 60
|
|
269
|
+
runs-on: ubuntu-latest
|
|
270
|
+
strategy:
|
|
271
|
+
fail-fast: false
|
|
272
|
+
matrix:
|
|
273
|
+
shard: [1, 2, 3, 4]
|
|
274
|
+
steps:
|
|
275
|
+
- uses: actions/checkout@v4
|
|
276
|
+
- uses: actions/setup-node@v4
|
|
277
|
+
with:
|
|
278
|
+
node-version: 18
|
|
279
|
+
- run: npm ci
|
|
280
|
+
- run: npx playwright install --with-deps
|
|
281
|
+
- run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
|
282
|
+
- uses: actions/upload-artifact@v4
|
|
283
|
+
if: always()
|
|
284
|
+
with:
|
|
285
|
+
name: pulse-report-shard-${{ matrix.shard }}
|
|
286
|
+
path: pulse-report/
|
|
287
|
+
retention-days: 1
|
|
288
|
+
|
|
289
|
+
merge-report:
|
|
290
|
+
needs: test
|
|
291
|
+
if: always()
|
|
292
|
+
runs-on: ubuntu-latest
|
|
293
|
+
steps:
|
|
294
|
+
- uses: actions/checkout@v4
|
|
295
|
+
- uses: actions/setup-node@v4
|
|
296
|
+
with:
|
|
297
|
+
node-version: 18
|
|
298
|
+
- run: npm ci
|
|
299
|
+
|
|
300
|
+
# Download all shard artifacts to a single directory
|
|
301
|
+
- uses: actions/download-artifact@v4
|
|
302
|
+
with:
|
|
303
|
+
path: all-reports
|
|
304
|
+
pattern: pulse-report-shard-*
|
|
305
|
+
|
|
306
|
+
# Merge all shard reports into a single report
|
|
307
|
+
- run: npx merge-pulse-report -o all-reports
|
|
308
|
+
|
|
309
|
+
# Generate the final HTML report
|
|
310
|
+
- run: npx generate-pulse-report -o all-reports
|
|
311
|
+
|
|
312
|
+
# Upload the final merged report
|
|
313
|
+
- uses: actions/upload-artifact@v4
|
|
314
|
+
with:
|
|
315
|
+
name: final-playwright-pulse-report
|
|
316
|
+
path: all-reports/
|
|
317
|
+
retention-days: 7
|
|
311
318
|
```
|
|
312
319
|
|
|
320
|
+
For more details, please refer to the [Pulse Report Sharded CI/CD Integration](https://arghajit47.github.io/playwright-pulse/sharding.html).
|
|
321
|
+
|
|
313
322
|
## 🧠 Notes
|
|
314
323
|
|
|
315
324
|
- <strong>`npm run generate-report` generates a HTML report ( screenshots/images will be taken in realtime from 'attachments/' directory ).</strong>
|
|
@@ -319,13 +328,14 @@ Under the hood, this will:
|
|
|
319
328
|
- After the test matrix completes, reports are downloaded, renamed, and merged.
|
|
320
329
|
- merge-report is a custom Node.js script that combines all JSON files into one.
|
|
321
330
|
|
|
322
|
-
## 
|
|
323
332
|
|
|
324
333
|
### 🚀 **Upgrade Now**
|
|
325
334
|
|
|
326
335
|
```bash
|
|
327
336
|
npm install @arghajit/playwright-pulse-report@latest
|
|
328
337
|
```
|
|
338
|
+
|
|
329
339
|
---
|
|
330
340
|
|
|
331
341
|
## ⚙️ Advanced Configuration
|
|
@@ -340,7 +350,7 @@ npx playwright test test1.spec.ts && npx playwright test test2.spec.ts
|
|
|
340
350
|
|
|
341
351
|
By default, In this above scenario, the report from test1 will be lost. To solve this, you can use the resetOnEachRun option.
|
|
342
352
|
|
|
343
|
-
```
|
|
353
|
+
```javascript
|
|
344
354
|
// playwright.config.ts
|
|
345
355
|
import { defineConfig } from "@playwright/test";
|
|
346
356
|
import * as path from "path";
|
|
@@ -396,7 +406,7 @@ npm run pulse-dashboard
|
|
|
396
406
|
|
|
397
407
|
*(Run from project root containing `pulse-report/` directory)*
|
|
398
408
|
|
|
399
|
-
**NPM Package**: [
|
|
409
|
+
**NPM Package**: [pulse-dashboard](https://www.npmjs.com/package/pulse-dashboard)
|
|
400
410
|
|
|
401
411
|
**Tech Stack**: Next.js, TypeScript, Tailwind CSS, Playwright
|
|
402
412
|
|
|
@@ -17,6 +17,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
17
17
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
18
18
|
onTestBegin(test: TestCase): void;
|
|
19
19
|
private _getSeverity;
|
|
20
|
+
private extractCodeSnippet;
|
|
20
21
|
private getBrowserDetails;
|
|
21
22
|
private processStep;
|
|
22
23
|
onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
|
|
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const crypto_1 = require("crypto");
|
|
43
43
|
const ua_parser_js_1 = __importDefault(require("ua-parser-js"));
|
|
44
44
|
const os = __importStar(require("os"));
|
|
45
|
+
const compression_utils_1 = require("../utils/compression-utils");
|
|
45
46
|
const convertStatus = (status, testCase) => {
|
|
46
47
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
47
48
|
return "failed";
|
|
@@ -116,6 +117,24 @@ class PlaywrightPulseReporter {
|
|
|
116
117
|
const severityAnnotation = annotations.find((a) => a.type === "pulse_severity");
|
|
117
118
|
return (severityAnnotation === null || severityAnnotation === void 0 ? void 0 : severityAnnotation.description) || "Medium";
|
|
118
119
|
}
|
|
120
|
+
extractCodeSnippet(filePath, targetLine, targetColumn) {
|
|
121
|
+
var _a;
|
|
122
|
+
try {
|
|
123
|
+
const fsSync = require('fs');
|
|
124
|
+
if (!fsSync.existsSync(filePath)) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
const content = fsSync.readFileSync(filePath, 'utf8');
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
if (targetLine < 1 || targetLine > lines.length) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
return ((_a = lines[targetLine - 1]) === null || _a === void 0 ? void 0 : _a.trim()) || '';
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
getBrowserDetails(test) {
|
|
120
139
|
var _a, _b, _c, _d;
|
|
121
140
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
@@ -180,8 +199,10 @@ class PlaywrightPulseReporter {
|
|
|
180
199
|
const startTime = new Date(step.startTime);
|
|
181
200
|
const endTime = new Date(startTime.getTime() + Math.max(0, duration));
|
|
182
201
|
let codeLocation = "";
|
|
202
|
+
let codeSnippet = '';
|
|
183
203
|
if (step.location) {
|
|
184
204
|
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
205
|
+
codeSnippet = this.extractCodeSnippet(step.location.file, step.location.line, step.location.column);
|
|
185
206
|
}
|
|
186
207
|
return {
|
|
187
208
|
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
@@ -194,6 +215,7 @@ class PlaywrightPulseReporter {
|
|
|
194
215
|
errorMessage: errorMessage,
|
|
195
216
|
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
|
|
196
217
|
codeLocation: codeLocation || undefined,
|
|
218
|
+
codeSnippet: codeSnippet,
|
|
197
219
|
isHook: step.category === "hook",
|
|
198
220
|
hookType: step.category === "hook"
|
|
199
221
|
? step.title.toLowerCase().includes("before")
|
|
@@ -204,10 +226,22 @@ class PlaywrightPulseReporter {
|
|
|
204
226
|
};
|
|
205
227
|
}
|
|
206
228
|
async onTestEnd(test, result) {
|
|
207
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p
|
|
229
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
208
230
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
209
231
|
const browserDetails = this.getBrowserDetails(test);
|
|
210
|
-
|
|
232
|
+
// Captured outcome from Playwright
|
|
233
|
+
const outcome = test.outcome();
|
|
234
|
+
// Calculate final status based on the last result (Last-Run-Wins)
|
|
235
|
+
// result.status in onTestEnd is typically the status of the test run (passed if flaky passed)
|
|
236
|
+
// But we double check the last result in test.results just to be sure/consistent
|
|
237
|
+
const lastResult = test.results[test.results.length - 1];
|
|
238
|
+
const finalStatus = convertStatus(lastResult ? lastResult.status : result.status, test);
|
|
239
|
+
// Existing behavior: fail if flaky (implied by user request "existing status field should remain failed")
|
|
240
|
+
// If outcome is flaky, status should be 'failed' to indicate initial failure, but final_status is 'passed'
|
|
241
|
+
let testStatus = finalStatus;
|
|
242
|
+
if (outcome === 'flaky') {
|
|
243
|
+
testStatus = 'flaky';
|
|
244
|
+
}
|
|
211
245
|
const startTime = new Date(result.startTime);
|
|
212
246
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
213
247
|
const processAllSteps = async (steps) => {
|
|
@@ -221,15 +255,9 @@ class PlaywrightPulseReporter {
|
|
|
221
255
|
}
|
|
222
256
|
return processed;
|
|
223
257
|
};
|
|
224
|
-
let codeSnippet =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const relativePath = path.relative(this.config.rootDir, test.location.file);
|
|
228
|
-
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (e) {
|
|
232
|
-
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
258
|
+
let codeSnippet = '';
|
|
259
|
+
if (((_b = test.location) === null || _b === void 0 ? void 0 : _b.file) && ((_c = test.location) === null || _c === void 0 ? void 0 : _c.line) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.column)) {
|
|
260
|
+
codeSnippet = this.extractCodeSnippet(test.location.file, test.location.line, test.location.column);
|
|
233
261
|
}
|
|
234
262
|
// 1. Get Spec File Name
|
|
235
263
|
const specFileName = ((_e = test.location) === null || _e === void 0 ? void 0 : _e.file)
|
|
@@ -263,15 +291,17 @@ class PlaywrightPulseReporter {
|
|
|
263
291
|
name: test.titlePath().join(" > "),
|
|
264
292
|
suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_g = this.config.projects[0]) === null || _g === void 0 ? void 0 : _g.name) || "Default Suite",
|
|
265
293
|
status: testStatus,
|
|
294
|
+
outcome: outcome === 'flaky' ? outcome : undefined, // Only Include if flaky
|
|
295
|
+
final_status: finalStatus, // New Field
|
|
266
296
|
duration: result.duration,
|
|
267
297
|
startTime: startTime,
|
|
268
298
|
endTime: endTime,
|
|
269
299
|
browser: browserDetails,
|
|
270
300
|
retries: result.retry,
|
|
271
|
-
steps:
|
|
272
|
-
errorMessage: (
|
|
273
|
-
stackTrace: (
|
|
274
|
-
snippet: (
|
|
301
|
+
steps: result.steps ? await processAllSteps(result.steps) : [],
|
|
302
|
+
errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
|
|
303
|
+
stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
|
|
304
|
+
snippet: (_k = result.error) === null || _k === void 0 ? void 0 : _k.snippet,
|
|
275
305
|
codeSnippet: codeSnippet,
|
|
276
306
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
277
307
|
severity: this._getSeverity(test.annotations),
|
|
@@ -281,7 +311,7 @@ class PlaywrightPulseReporter {
|
|
|
281
311
|
attachments: [],
|
|
282
312
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
283
313
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
284
|
-
annotations: ((
|
|
314
|
+
annotations: ((_l = test.annotations) === null || _l === void 0 ? void 0 : _l.length) > 0 ? test.annotations : undefined,
|
|
285
315
|
...testSpecificData,
|
|
286
316
|
};
|
|
287
317
|
for (const [index, attachment] of result.attachments.entries()) {
|
|
@@ -296,18 +326,21 @@ class PlaywrightPulseReporter {
|
|
|
296
326
|
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
|
|
297
327
|
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
|
|
298
328
|
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
329
|
+
// Copy file first
|
|
299
330
|
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
331
|
+
// Compress in-place (preserves path/name)
|
|
332
|
+
await (0, compression_utils_1.compressAttachment)(absoluteDestPath, attachment.contentType);
|
|
300
333
|
if (attachment.contentType.startsWith("image/")) {
|
|
301
|
-
(
|
|
334
|
+
(_m = pulseResult.screenshots) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
|
|
302
335
|
}
|
|
303
336
|
else if (attachment.contentType.startsWith("video/")) {
|
|
304
|
-
(
|
|
337
|
+
(_o = pulseResult.videoPath) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
|
|
305
338
|
}
|
|
306
339
|
else if (attachment.name === "trace") {
|
|
307
340
|
pulseResult.tracePath = relativeDestPath;
|
|
308
341
|
}
|
|
309
342
|
else {
|
|
310
|
-
(
|
|
343
|
+
(_p = pulseResult.attachments) === null || _p === void 0 ? void 0 : _p.push({
|
|
311
344
|
name: attachment.name,
|
|
312
345
|
path: relativeDestPath,
|
|
313
346
|
contentType: attachment.contentType,
|
|
@@ -321,15 +354,40 @@ class PlaywrightPulseReporter {
|
|
|
321
354
|
this.results.push(pulseResult);
|
|
322
355
|
}
|
|
323
356
|
_getFinalizedResults(allResults) {
|
|
324
|
-
const
|
|
357
|
+
const resultsMap = new Map();
|
|
325
358
|
for (const result of allResults) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
359
|
+
if (!resultsMap.has(result.id)) {
|
|
360
|
+
resultsMap.set(result.id, []);
|
|
361
|
+
}
|
|
362
|
+
resultsMap.get(result.id).push(result);
|
|
363
|
+
}
|
|
364
|
+
const finalResults = [];
|
|
365
|
+
for (const [testId, attempts] of resultsMap.entries()) {
|
|
366
|
+
attempts.sort((a, b) => a.retries - b.retries);
|
|
367
|
+
const firstAttempt = attempts[0];
|
|
368
|
+
const retryAttempts = attempts.slice(1);
|
|
369
|
+
// Only populate retryHistory if there were actual failures that triggered retries
|
|
370
|
+
// If all attempts passed, we don't need to show retry history
|
|
371
|
+
const hasActualRetries = retryAttempts.length > 0 && retryAttempts.some(attempt => attempt.status === 'failed' || attempt.status === 'flaky' || firstAttempt.status === 'failed' || firstAttempt.status === 'flaky');
|
|
372
|
+
if (hasActualRetries) {
|
|
373
|
+
firstAttempt.retryHistory = retryAttempts;
|
|
374
|
+
// Calculate final status and outcome from the last attempt if retries exist
|
|
375
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
376
|
+
firstAttempt.final_status = lastAttempt.status;
|
|
377
|
+
// If the last attempt was flaky, ensure outcome is set on the main result
|
|
378
|
+
if (lastAttempt.outcome === 'flaky' || lastAttempt.status === 'flaky') {
|
|
379
|
+
firstAttempt.outcome = 'flaky';
|
|
380
|
+
firstAttempt.status = 'flaky';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// If no actual retries (all attempts passed), ensure final_status and retryHistory are removed
|
|
385
|
+
delete firstAttempt.final_status;
|
|
386
|
+
delete firstAttempt.retryHistory;
|
|
330
387
|
}
|
|
388
|
+
finalResults.push(firstAttempt);
|
|
331
389
|
}
|
|
332
|
-
return
|
|
390
|
+
return finalResults;
|
|
333
391
|
}
|
|
334
392
|
onError(error) {
|
|
335
393
|
var _a;
|
|
@@ -389,6 +447,7 @@ class PlaywrightPulseReporter {
|
|
|
389
447
|
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
390
448
|
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
391
449
|
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
|
|
450
|
+
finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
|
|
392
451
|
finalRunData.totalTests = finalResultsList.length;
|
|
393
452
|
const reviveDates = (key, value) => {
|
|
394
453
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
@@ -449,6 +508,7 @@ class PlaywrightPulseReporter {
|
|
|
449
508
|
passed: finalResults.filter((r) => r.status === "passed").length,
|
|
450
509
|
failed: finalResults.filter((r) => r.status === "failed").length,
|
|
451
510
|
skipped: finalResults.filter((r) => r.status === "skipped").length,
|
|
511
|
+
flaky: finalResults.filter((r) => r.status === "flaky").length,
|
|
452
512
|
duration,
|
|
453
513
|
environment: environmentDetails,
|
|
454
514
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LucideIcon } from "lucide-react";
|
|
2
|
-
export type TestStatus = "passed" | "failed" | "skipped" | "expected-failure" | "unexpected-success" | "explicitly-skipped";
|
|
2
|
+
export type TestStatus = "passed" | "failed" | "skipped" | "expected-failure" | "unexpected-success" | "explicitly-skipped" | "flaky";
|
|
3
3
|
export interface TestStep {
|
|
4
4
|
id: string;
|
|
5
5
|
title: string;
|
|
@@ -11,6 +11,7 @@ export interface TestStep {
|
|
|
11
11
|
errorMessage?: string;
|
|
12
12
|
stackTrace?: string;
|
|
13
13
|
codeLocation?: string;
|
|
14
|
+
codeSnippet?: string;
|
|
14
15
|
isHook?: boolean;
|
|
15
16
|
hookType?: "before" | "after";
|
|
16
17
|
steps?: TestStep[];
|
|
@@ -35,6 +36,8 @@ export interface TestResult {
|
|
|
35
36
|
suiteName?: string;
|
|
36
37
|
runId: string;
|
|
37
38
|
browser: string;
|
|
39
|
+
outcome?: string;
|
|
40
|
+
final_status?: TestStatus;
|
|
38
41
|
screenshots?: string[];
|
|
39
42
|
videoPath?: string[];
|
|
40
43
|
tracePath?: string;
|
|
@@ -58,6 +61,7 @@ export interface TestResult {
|
|
|
58
61
|
column: number;
|
|
59
62
|
};
|
|
60
63
|
}[];
|
|
64
|
+
retryHistory?: TestResult[];
|
|
61
65
|
}
|
|
62
66
|
export interface TestRun {
|
|
63
67
|
id: string;
|
|
@@ -66,14 +70,16 @@ export interface TestRun {
|
|
|
66
70
|
passed: number;
|
|
67
71
|
failed: number;
|
|
68
72
|
skipped: number;
|
|
73
|
+
flaky?: number;
|
|
69
74
|
duration: number;
|
|
70
|
-
environment?: EnvDetails;
|
|
75
|
+
environment?: EnvDetails | EnvDetails[];
|
|
71
76
|
}
|
|
72
77
|
export interface TrendDataPoint {
|
|
73
78
|
date: string;
|
|
74
79
|
passed: number;
|
|
75
80
|
failed: number;
|
|
76
81
|
skipped: number;
|
|
82
|
+
flaky?: number;
|
|
77
83
|
}
|
|
78
84
|
export interface SummaryMetric {
|
|
79
85
|
label: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression utilities for images
|
|
3
|
+
* Uses sharp for image compression (works cross-platform with no external dependencies)
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Compress an image file in-place
|
|
7
|
+
* @param filePath - Absolute path to the image file
|
|
8
|
+
* @param options - Compression options
|
|
9
|
+
*/
|
|
10
|
+
export declare function compressImage(filePath: string, options?: {
|
|
11
|
+
quality?: number;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Compress an attachment file (auto-detects type)
|
|
15
|
+
* Note: Only compresses images. Videos are already compressed by Playwright.
|
|
16
|
+
* @param filePath - Absolute path to the file
|
|
17
|
+
* @param contentType - MIME content type
|
|
18
|
+
*/
|
|
19
|
+
export declare function compressAttachment(filePath: string, contentType: string): Promise<void>;
|