@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 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://pulse-report.netlify.app/)
11
+ ## [Live Demo](https://arghajit47.github.io/playwright-pulse/demo.html)
12
12
 
13
- ## ![Features](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/features.svg)
13
+ ## ![Features](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/features.svg)
14
14
 
15
- ## **Documentation**: [Pulse Report](https://playwright-pulse-report.netlify.app/)
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 embadded in the report, no need to have attachments/ directory when viewing the report, with a dark theme and better initial load handling |
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
- ```typescript
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
+ ![Custom Output Directory](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/custom-output-directory-config.png)
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
- # Upload Pulse report from each shard (per matrix.config.type)
244
- - name: Upload Pulse Report results
245
- if: success() || failure()
246
- uses: actions/upload-artifact@v4
247
- with:
248
- name: pulse-report
249
- path: pulse-report/
250
-
251
- # Download all pulse-report-* artifacts after all shards complete
252
- - name: Download Pulse Report artifacts
253
- uses: actions/download-artifact@v4
254
- with:
255
- pattern: pulse-report
256
- path: downloaded-artifacts
257
-
258
- # Merge all sharded JSON reports into one final output
259
- - name: Generate Pulse Report
260
- run: |
261
- npm run script merge-report
262
- npm run generate-report [or, npm run generate-pulse-report]
263
-
264
- # Upload final merged report as CI artifact
265
- - name: Upload Pulse report
266
- uses: actions/upload-artifact@v4
267
- with:
268
- name: pulse-report
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
- # Upload Pulse report from each shard (per matrix.config.type)
275
- - name: Upload Pulse Report results
276
- if: success() || failure()
277
- uses: actions/upload-artifact@v4
278
- with:
279
- name: pulse-report-${{ matrix.config.type }}
280
- path: pulse-report/
281
-
282
- # Download all pulse-report-* artifacts after all shards complete
283
- - name: Download Pulse Report artifacts
284
- uses: actions/download-artifact@v4
285
- with:
286
- pattern: pulse-report-*
287
- path: downloaded-artifacts
288
-
289
- # Organize reports into a single folder and rename for merging
290
- - name: Organize Pulse Report
291
- run: |
292
- mkdir -p pulse-report
293
- for dir in downloaded-artifacts/pulse-report-*; do
294
- config_type=$(basename "$dir" | sed 's/pulse-report-//')
295
- cp -r "$dir/attachments" "pulse-report/attachments"
296
- cp "$dir/playwright-pulse-report.json" "pulse-report/playwright-pulse-report-${config_type}.json"
297
- done
298
-
299
- # Merge all sharded JSON reports into one final output
300
- - name: Generate Pulse Report
301
- run: |
302
- npm run merge-report
303
- npm run generate-report [or, npm run generate-pulse-report]
304
-
305
- # Upload final merged report as CI artifact
306
- - name: Upload Pulse report
307
- uses: actions/upload-artifact@v4
308
- with:
309
- name: pulse-report
310
- path: pulse-report/
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
- ## ![Features](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-folder-structures.svg)
331
+ ## ![Folder-Structure](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/pulse-folder-structures.svg)
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
- ```bash
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**: [playwright-pulse-report](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
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, _q;
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
- const testStatus = convertStatus(result.status, test);
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 = undefined;
225
- try {
226
- 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)) {
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: ((_h = result.steps) === null || _h === void 0 ? void 0 : _h.length) ? await processAllSteps(result.steps) : [],
272
- errorMessage: (_j = result.error) === null || _j === void 0 ? void 0 : _j.message,
273
- stackTrace: (_k = result.error) === null || _k === void 0 ? void 0 : _k.stack,
274
- snippet: (_l = result.error) === null || _l === void 0 ? void 0 : _l.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: ((_m = test.annotations) === null || _m === void 0 ? void 0 : _m.length) > 0 ? test.annotations : undefined,
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
- (_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
334
+ (_m = pulseResult.screenshots) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
302
335
  }
303
336
  else if (attachment.contentType.startsWith("video/")) {
304
- (_p = pulseResult.videoPath) === null || _p === void 0 ? void 0 : _p.push(relativeDestPath);
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
- (_q = pulseResult.attachments) === null || _q === void 0 ? void 0 : _q.push({
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 finalResultsMap = new Map();
357
+ const resultsMap = new Map();
325
358
  for (const result of allResults) {
326
- const existing = finalResultsMap.get(result.id);
327
- // Keep the result with the highest retry attempt for each test ID
328
- if (!existing || result.retries >= existing.retries) {
329
- finalResultsMap.set(result.id, result);
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 Array.from(finalResultsMap.values());
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
  };
@@ -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>;