@arghajit/dummy 0.1.2 → 0.3.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 CHANGED
@@ -1,6 +1,11 @@
1
1
  # Playwright Pluse Report
2
2
 
3
- ![Playwright Pulse Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/image.png)
3
+ [![NPM Version](https://img.shields.io/npm/v/@arghajit/playwright-pulse-report.svg)](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![NPM Downloads](https://img.shields.io/npm/dm/@arghajit/playwright-pulse-report.svg)](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
6
+
7
+ ![Playwright Pulse Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright-pulse-report.png)
8
+
4
9
  _The ultimate Playwright reporter — Interactive dashboard with historical trend analytics, CI/CD-ready standalone HTML reports, and sharding support for scalable test execution._
5
10
 
6
11
  ## [Live Demo](https://pulse-report.netlify.app/)
@@ -16,7 +21,7 @@ The project provides these utility commands:
16
21
  | Command | Description |
17
22
  |------------------------|-----------------------------------------------------------------------------|
18
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 |
19
- | `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 |
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 |
20
25
  | `merge-pulse-report` | Combines multiple parallel test json reports, basically used in sharding |
21
26
  | `generate-trend` | Analyzes historical trends in test results |
22
27
  | `generate-email-report`| Generates email-friendly report versions |
@@ -272,8 +277,7 @@ export default defineConfig({
272
277
 
273
278
  ---
274
279
 
275
- <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-logo.png" alt="pulse dashboard" title="pulse dashboard" height="35px" width="60px" align="left" padding="5px"/>
276
- <h2>Pulse Dashboard</h2>
280
+ ![pulse dashboard](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/pulse_dashboard_full_icon.png)
277
281
 
278
282
  **Real-time Playwright Test Monitoring & Analysis**
279
283
 
@@ -12,7 +12,6 @@ export declare class PlaywrightPulseReporter implements Reporter {
12
12
  private isSharded;
13
13
  private shardIndex;
14
14
  private resetOnEachRun;
15
- private currentRunId;
16
15
  constructor(options?: PlaywrightPulseReporterOptions);
17
16
  printsToStdio(): boolean;
18
17
  onBegin(config: FullConfig, suite: Suite): void;
@@ -21,7 +20,6 @@ export declare class PlaywrightPulseReporter implements Reporter {
21
20
  private processStep;
22
21
  onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
23
22
  private _getFinalizedResults;
24
- private _getStatusOrder;
25
23
  onError(error: any): void;
26
24
  private _getEnvDetails;
27
25
  private _writeShardResults;
@@ -39,20 +39,13 @@ const path = __importStar(require("path"));
39
39
  const crypto_1 = require("crypto");
40
40
  const ua_parser_js_1 = require("ua-parser-js");
41
41
  const os = __importStar(require("os"));
42
- const convertStatus = (status, testCase, retryCount = 0) => {
42
+ const convertStatus = (status, testCase) => {
43
43
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
- // If expected to fail but passed, it's flaky
45
- if (status === "passed")
46
- return "flaky";
47
44
  return "failed";
48
45
  }
49
46
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
50
47
  return "skipped";
51
48
  }
52
- // If a test passes on a retry, it's considered flaky
53
- if (status === "passed" && retryCount > 0) {
54
- return "flaky";
55
- }
56
49
  switch (status) {
57
50
  case "passed":
58
51
  return "passed";
@@ -75,7 +68,6 @@ class PlaywrightPulseReporter {
75
68
  this.baseOutputFile = "playwright-pulse-report.json";
76
69
  this.isSharded = false;
77
70
  this.shardIndex = undefined;
78
- this.currentRunId = ""; // Added to store the overall run ID
79
71
  this.options = options;
80
72
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
81
73
  this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
@@ -90,8 +82,6 @@ class PlaywrightPulseReporter {
90
82
  this.config = config;
91
83
  this.suite = suite;
92
84
  this.runStartTime = Date.now();
93
- // Generate the overall runId once at the beginning
94
- this.currentRunId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
95
85
  const configDir = this.config.rootDir;
96
86
  const configFileDir = this.config.configFile
97
87
  ? path.dirname(this.config.configFile)
@@ -117,7 +107,7 @@ class PlaywrightPulseReporter {
117
107
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
118
108
  }
119
109
  onTestBegin(test) {
120
- // console.log(`Starting test: ${test.title}`); // Removed for brevity in final output
110
+ console.log(`Starting test: ${test.title}`);
121
111
  }
122
112
  getBrowserDetails(test) {
123
113
  var _a, _b, _c, _d;
@@ -169,8 +159,7 @@ class PlaywrightPulseReporter {
169
159
  }
170
160
  return finalString.trim();
171
161
  }
172
- async processStep(step, testId, browserDetails, testCase, retryCount = 0 // Pass retryCount to convertStatus for steps
173
- ) {
162
+ async processStep(step, testId, browserDetails, testCase) {
174
163
  var _a, _b, _c, _d;
175
164
  let stepStatus = "passed";
176
165
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
@@ -178,8 +167,7 @@ class PlaywrightPulseReporter {
178
167
  stepStatus = "skipped";
179
168
  }
180
169
  else {
181
- // Use the extended convertStatus
182
- stepStatus = convertStatus(step.error ? "failed" : "passed", testCase, retryCount);
170
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
183
171
  }
184
172
  const duration = step.duration;
185
173
  const startTime = new Date(step.startTime);
@@ -209,18 +197,16 @@ class PlaywrightPulseReporter {
209
197
  };
210
198
  }
211
199
  async onTestEnd(test, result) {
212
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
200
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
213
201
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
214
202
  const browserDetails = this.getBrowserDetails(test);
215
- // Use the extended convertStatus, passing result.retry
216
- const testStatus = convertStatus(result.status, test, result.retry);
203
+ const testStatus = convertStatus(result.status, test);
217
204
  const startTime = new Date(result.startTime);
218
205
  const endTime = new Date(startTime.getTime() + result.duration);
219
206
  const processAllSteps = async (steps) => {
220
207
  let processed = [];
221
208
  for (const step of steps) {
222
- const processedStep = await this.processStep(step, test.id, browserDetails, test, result.retry // Pass retryCount to processStep
223
- );
209
+ const processedStep = await this.processStep(step, test.id, browserDetails, test);
224
210
  processed.push(processedStep);
225
211
  if (step.steps && step.steps.length > 0) {
226
212
  processedStep.steps = await processAllSteps(step.steps);
@@ -252,11 +238,9 @@ class PlaywrightPulseReporter {
252
238
  ? JSON.stringify(this.config.metadata)
253
239
  : undefined,
254
240
  };
255
- // Modify test.id for retries
256
- const testIdWithRunCounter = result.retry > 0 ? `${test.id}-${result.retry}` : test.id;
257
241
  const pulseResult = {
258
- id: testIdWithRunCounter, // Use the modified ID
259
- runId: this.currentRunId, // Assign the overall run ID
242
+ id: test.id,
243
+ runId: "TBD",
260
244
  name: test.titlePath().join(" > "),
261
245
  suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_e = this.config.projects[0]) === null || _e === void 0 ? void 0 : _e.name) || "Default Suite",
262
246
  status: testStatus,
@@ -264,8 +248,7 @@ class PlaywrightPulseReporter {
264
248
  startTime: startTime,
265
249
  endTime: endTime,
266
250
  browser: browserDetails,
267
- retries: result.retry, // This remains the Playwright retry count (0 for first run, 1 for first retry, etc.)
268
- runCounter: result.retry, // This is your 'runCounter'
251
+ retries: result.retry,
269
252
  steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
270
253
  errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
271
254
  stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
@@ -278,14 +261,14 @@ class PlaywrightPulseReporter {
278
261
  attachments: [],
279
262
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
280
263
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
264
+ annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
281
265
  ...testSpecificData,
282
266
  };
283
267
  for (const [index, attachment] of result.attachments.entries()) {
284
268
  if (!attachment.path)
285
269
  continue;
286
270
  try {
287
- // Use the new testIdWithRunCounter for the subfolder
288
- const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
271
+ const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
289
272
  const safeAttachmentName = path
290
273
  .basename(attachment.path)
291
274
  .replace(/[^a-zA-Z0-9_.-]/g, "_");
@@ -295,16 +278,16 @@ class PlaywrightPulseReporter {
295
278
  await this._ensureDirExists(path.dirname(absoluteDestPath));
296
279
  await fs.copyFile(attachment.path, absoluteDestPath);
297
280
  if (attachment.contentType.startsWith("image/")) {
298
- (_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
281
+ (_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
299
282
  }
300
283
  else if (attachment.contentType.startsWith("video/")) {
301
- (_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
284
+ (_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
302
285
  }
303
286
  else if (attachment.name === "trace") {
304
287
  pulseResult.tracePath = relativeDestPath;
305
288
  }
306
289
  else {
307
- (_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
290
+ (_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
308
291
  name: attachment.name,
309
292
  path: relativeDestPath,
310
293
  contentType: attachment.contentType,
@@ -320,46 +303,14 @@ class PlaywrightPulseReporter {
320
303
  _getFinalizedResults(allResults) {
321
304
  const finalResultsMap = new Map();
322
305
  for (const result of allResults) {
323
- // The key for de-duplication should now be the base test ID (without the run counter suffix)
324
- // This ensures that all runs of a single logical test are considered together.
325
- const baseTestId = result.id.split("-").slice(0, -1).join("-"); // Remove '-${runCounter}'
326
- const existing = finalResultsMap.get(baseTestId);
327
- // We want to keep the "most successful" run for the final report.
328
- // Priority: passed > flaky > failed > skipped.
329
- // If statuses are equal, prefer the one with higher retry count (latest attempt).
330
- if (!existing) {
331
- finalResultsMap.set(baseTestId, result);
332
- }
333
- else {
334
- const currentStatusOrder = this._getStatusOrder(result.status);
335
- const existingStatusOrder = this._getStatusOrder(existing.status);
336
- if (currentStatusOrder < existingStatusOrder) {
337
- // Current result is "better" (e.g., passed over failed)
338
- finalResultsMap.set(baseTestId, result);
339
- }
340
- else if (currentStatusOrder === existingStatusOrder &&
341
- result.retries > existing.retries) {
342
- // Same status, but current is a later retry, so prefer it
343
- finalResultsMap.set(baseTestId, result);
344
- }
306
+ const existing = finalResultsMap.get(result.id);
307
+ // Keep the result with the highest retry attempt for each test ID
308
+ if (!existing || result.retries >= existing.retries) {
309
+ finalResultsMap.set(result.id, result);
345
310
  }
346
311
  }
347
312
  return Array.from(finalResultsMap.values());
348
313
  }
349
- _getStatusOrder(status) {
350
- switch (status) {
351
- case "passed":
352
- return 1;
353
- case "flaky":
354
- return 2;
355
- case "failed":
356
- return 3;
357
- case "skipped":
358
- return 4;
359
- default:
360
- return 99; // Unknown status
361
- }
362
- }
363
314
  onError(error) {
364
315
  var _a;
365
316
  console.error(`PlaywrightPulseReporter: Error encountered (Shard: ${(_a = this.shardIndex) !== null && _a !== void 0 ? _a : "Main"}):`, (error === null || error === void 0 ? void 0 : error.message) || error);
@@ -394,14 +345,15 @@ class PlaywrightPulseReporter {
394
345
  }
395
346
  }
396
347
  async _mergeShardResults(finalRunData) {
397
- let allShardRawResults = []; // Store raw results before final de-duplication
348
+ let allShardProcessedResults = [];
398
349
  const totalShards = this.config.shard ? this.config.shard.total : 1;
399
350
  for (let i = 0; i < totalShards; i++) {
400
351
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
401
352
  try {
402
353
  const content = await fs.readFile(tempFilePath, "utf-8");
403
354
  const shardResults = JSON.parse(content);
404
- allShardRawResults = allShardRawResults.concat(shardResults);
355
+ allShardProcessedResults =
356
+ allShardProcessedResults.concat(shardResults);
405
357
  }
406
358
  catch (error) {
407
359
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
@@ -412,14 +364,11 @@ class PlaywrightPulseReporter {
412
364
  }
413
365
  }
414
366
  }
415
- // Apply _getFinalizedResults after all raw shard results are collected
416
- const finalResultsList = this._getFinalizedResults(allShardRawResults);
367
+ const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
417
368
  finalResultsList.forEach((r) => (r.runId = finalRunData.id));
418
369
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
419
370
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
420
371
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
421
- // Add flaky count
422
- finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
423
372
  finalRunData.totalTests = finalResultsList.length;
424
373
  const reviveDates = (key, value) => {
425
374
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -466,36 +415,34 @@ class PlaywrightPulseReporter {
466
415
  await this._writeShardResults();
467
416
  return;
468
417
  }
469
- // `this.results` now contains all individual run attempts.
470
- // _getFinalizedResults will select the "best" run for each logical test.
418
+ // De-duplicate and handle retries here, in a safe, single-threaded context.
471
419
  const finalResults = this._getFinalizedResults(this.results);
472
420
  const runEndTime = Date.now();
473
421
  const duration = runEndTime - this.runStartTime;
474
- // Use the stored overall runId
475
- const runId = this.currentRunId;
422
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
476
423
  const environmentDetails = this._getEnvDetails();
477
424
  const runData = {
478
425
  id: runId,
479
426
  timestamp: new Date(this.runStartTime),
427
+ // Use the length of the de-duplicated array for all counts
480
428
  totalTests: finalResults.length,
481
429
  passed: finalResults.filter((r) => r.status === "passed").length,
482
430
  failed: finalResults.filter((r) => r.status === "failed").length,
483
431
  skipped: finalResults.filter((r) => r.status === "skipped").length,
484
- flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
485
432
  duration,
486
433
  environment: environmentDetails,
487
434
  };
488
- // Ensure all final results have the correct overall runId
489
435
  finalResults.forEach((r) => (r.runId = runId));
490
436
  let finalReport = undefined;
491
437
  if (this.isSharded) {
492
- // _mergeShardResults will now perform the final de-duplication across shards
438
+ // The _mergeShardResults method will handle its own de-duplication
493
439
  finalReport = await this._mergeShardResults(runData);
494
440
  }
495
441
  else {
496
442
  finalReport = {
497
443
  run: runData,
498
- results: finalResults, // Use the de-duplicated results for a non-sharded run
444
+ // Use the de-duplicated results
445
+ results: finalResults,
499
446
  metadata: { generatedAt: new Date().toISOString() },
500
447
  };
501
448
  }
@@ -524,6 +471,7 @@ class PlaywrightPulseReporter {
524
471
  }
525
472
  }
526
473
  else {
474
+ // Logic for appending/merging reports
527
475
  const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
528
476
  const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
529
477
  try {
@@ -569,10 +517,7 @@ class PlaywrightPulseReporter {
569
517
  const allResultsFromAllFiles = [];
570
518
  let latestTimestamp = new Date(0);
571
519
  let lastRunEnvironment = undefined;
572
- // We can't simply sum durations across merged files, as the tests might overlap.
573
- // The final duration will be derived from the range of start/end times in the final results.
574
- let earliestStartTime = Date.now();
575
- let latestEndTime = 0;
520
+ let totalDuration = 0;
576
521
  for (const file of reportFiles) {
577
522
  const filePath = path.join(pulseResultsDir, file);
578
523
  try {
@@ -595,28 +540,22 @@ class PlaywrightPulseReporter {
595
540
  }
596
541
  // De-duplicate the results from ALL merged files using the helper function
597
542
  const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
598
- // Calculate overall duration from the earliest start and latest end of the final merged results
599
- for (const res of finalMergedResults) {
600
- if (res.startTime.getTime() < earliestStartTime)
601
- earliestStartTime = res.startTime.getTime();
602
- if (res.endTime.getTime() > latestEndTime)
603
- latestEndTime = res.endTime.getTime();
604
- }
605
- const totalDuration = latestEndTime > earliestStartTime ? latestEndTime - earliestStartTime : 0;
543
+ // Sum the duration from the final, de-duplicated list of tests
544
+ totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
606
545
  const combinedRun = {
607
546
  id: `merged-${Date.now()}`,
608
547
  timestamp: latestTimestamp,
609
548
  environment: lastRunEnvironment,
549
+ // Recalculate counts based on the truly final, de-duplicated list
610
550
  totalTests: finalMergedResults.length,
611
551
  passed: finalMergedResults.filter((r) => r.status === "passed").length,
612
552
  failed: finalMergedResults.filter((r) => r.status === "failed").length,
613
553
  skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
614
- flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
615
554
  duration: totalDuration,
616
555
  };
617
556
  const finalReport = {
618
557
  run: combinedRun,
619
- results: finalMergedResults,
558
+ results: finalMergedResults, // Use the de-duplicated list
620
559
  metadata: {
621
560
  generatedAt: new Date().toISOString(),
622
561
  },
@@ -1,10 +1,9 @@
1
1
  import type { LucideIcon } from "lucide-react";
2
2
  export type TestStatus = "passed" | "failed" | "skipped" | "expected-failure" | "unexpected-success" | "explicitly-skipped";
3
- export type PulseTestStatus = TestStatus | "flaky";
4
3
  export interface TestStep {
5
4
  id: string;
6
5
  title: string;
7
- status: PulseTestStatus;
6
+ status: TestStatus;
8
7
  duration: number;
9
8
  startTime: Date;
10
9
  endTime: Date;
@@ -19,12 +18,11 @@ export interface TestStep {
19
18
  export interface TestResult {
20
19
  id: string;
21
20
  name: string;
22
- status: PulseTestStatus;
21
+ status: TestStatus;
23
22
  duration: number;
24
23
  startTime: Date;
25
24
  endTime: Date;
26
25
  retries: number;
27
- runCounter: number;
28
26
  steps: TestStep[];
29
27
  errorMessage?: string;
30
28
  stackTrace?: string;
@@ -48,6 +46,15 @@ export interface TestResult {
48
46
  totalWorkers?: number;
49
47
  configFile?: string;
50
48
  metadata?: string;
49
+ annotations?: {
50
+ type: string;
51
+ description?: string;
52
+ location?: {
53
+ file: string;
54
+ line: number;
55
+ column: number;
56
+ };
57
+ }[];
51
58
  }
52
59
  export interface TestRun {
53
60
  id: string;
@@ -56,7 +63,6 @@ export interface TestRun {
56
63
  passed: number;
57
64
  failed: number;
58
65
  skipped: number;
59
- flaky: number;
60
66
  duration: number;
61
67
  environment?: EnvDetails;
62
68
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
- "version": "0.1.2",
3
+ "author": "Arghajit Singha",
4
+ "version": "0.3.0",
4
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
5
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
6
7
  "keywords": [
@@ -17,7 +18,8 @@
17
18
  "send-report",
18
19
  "email",
19
20
  "playwright-report",
20
- "pulse"
21
+ "pulse",
22
+ "ai-failure-analysis"
21
23
  ],
22
24
  "main": "dist/reporter/index.js",
23
25
  "types": "dist/reporter/index.d.ts",
@@ -72,13 +74,16 @@
72
74
  "devDependencies": {
73
75
  "@types/node": "^20",
74
76
  "@types/ua-parser-js": "^0.7.39",
75
- "eslint": "9.25.1",
77
+ "eslint": "^9.39.1",
76
78
  "typescript": "^5"
77
79
  },
78
80
  "engines": {
79
- "node": ">=16"
81
+ "node": ">=18"
80
82
  },
81
83
  "peerDependencies": {
82
84
  "@playwright/test": ">=1.40.0"
85
+ },
86
+ "overrides": {
87
+ "glob": "^13.0.0"
83
88
  }
84
89
  }
@@ -1713,41 +1713,86 @@ function generateHTML(reportData, trendData = null) {
1713
1713
  };
1714
1714
 
1715
1715
  return `
1716
- <div class="test-case" data-status="${test.status
1717
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1718
- .join(",")
1719
- .toLowerCase()}">
1716
+ <div class="test-case" data-status="${
1717
+ test.status
1718
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1719
+ .join(",")
1720
+ .toLowerCase()}">
1720
1721
  <div class="test-case-header" role="button" aria-expanded="false">
1721
1722
  <div class="test-case-summary">
1722
1723
  <span class="status-badge ${getStatusClass(test.status)}">${String(
1723
- test.status
1724
- ).toUpperCase()}</span>
1724
+ test.status
1725
+ ).toUpperCase()}</span>
1725
1726
  <span class="test-case-title" title="${sanitizeHTML(
1726
1727
  test.name
1727
1728
  )}">${sanitizeHTML(testTitle)}</span>
1728
1729
  <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
1729
1730
  </div>
1730
1731
  <div class="test-case-meta">
1731
- ${test.tags && test.tags.length > 0
1732
- ? test.tags
1733
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1734
- .join(" ")
1735
- : ""
1736
- }
1732
+ ${
1733
+ test.tags && test.tags.length > 0
1734
+ ? test.tags
1735
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1736
+ .join(" ")
1737
+ : ""
1738
+ }
1737
1739
  <span class="test-duration">${formatDuration(test.duration)}</span>
1738
1740
  </div>
1739
1741
  </div>
1740
1742
  <div class="test-case-content" style="display: none;">
1741
1743
  <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
1744
+ ${
1745
+ test.annotations && test.annotations.length > 0
1746
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
1747
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
1748
+ ${test.annotations
1749
+ .map((annotation, idx) => {
1750
+ const isIssueOrBug =
1751
+ annotation.type === "issue" ||
1752
+ annotation.type === "bug";
1753
+ const descriptionText = annotation.description || "";
1754
+ const typeLabel = sanitizeHTML(annotation.type);
1755
+ const descriptionHtml =
1756
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
1757
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
1758
+ descriptionText
1759
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
1760
+ descriptionText
1761
+ )}</a>`
1762
+ : sanitizeHTML(descriptionText);
1763
+ const locationText = annotation.location
1764
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
1765
+ annotation.location.file
1766
+ )}:${annotation.location.line}:${
1767
+ annotation.location.column
1768
+ }</div>`
1769
+ : "";
1770
+ return `<div style="margin-bottom: ${
1771
+ idx < test.annotations.length - 1 ? "10px" : "0"
1772
+ };">
1773
+ <strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
1774
+ ${
1775
+ descriptionText
1776
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
1777
+ : ""
1778
+ }
1779
+ ${locationText}
1780
+ </div>`;
1781
+ })
1782
+ .join("")}
1783
+ </div>`
1784
+ : ""
1785
+ }
1742
1786
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1743
1787
  test.workerId
1744
1788
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1745
- test.totalWorkers
1746
- )}]</p>
1747
- ${test.errorMessage
1748
- ? `<div class="test-error-summary">${formatPlaywrightError(
1749
- test.errorMessage
1750
- )}
1789
+ test.totalWorkers
1790
+ )}]</p>
1791
+ ${
1792
+ test.errorMessage
1793
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1794
+ test.errorMessage
1795
+ )}
1751
1796
  <button
1752
1797
  class="copy-error-btn"
1753
1798
  onclick="copyErrorToClipboard(this)"
@@ -1768,13 +1813,14 @@ function generateHTML(reportData, trendData = null) {
1768
1813
  Copy Error Prompt
1769
1814
  </button>
1770
1815
  </div>`
1771
- : ""
1816
+ : ""
1772
1817
  }
1773
- ${test.snippet
1774
- ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1775
- test.snippet
1776
- )}</code></pre></div>`
1777
- : ""
1818
+ ${
1819
+ test.snippet
1820
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1821
+ test.snippet
1822
+ )}</code></pre></div>`
1823
+ : ""
1778
1824
  }
1779
1825
  <h4>Steps</h4>
1780
1826
  <div class="steps-list">${generateStepsHTML(test.steps)}</div>
@@ -1793,75 +1839,86 @@ function generateHTML(reportData, trendData = null) {
1793
1839
  </div>
1794
1840
  </div>`;
1795
1841
  })()}
1796
- ${test.stderr && test.stderr.length > 0
1797
- ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
1798
- test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1799
- )}</pre></div>`
1800
- : ""
1842
+ ${
1843
+ test.stderr && test.stderr.length > 0
1844
+ ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
1845
+ test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1846
+ )}</pre></div>`
1847
+ : ""
1801
1848
  }
1802
- ${test.screenshots && test.screenshots.length > 0
1803
- ? `
1849
+ ${
1850
+ test.screenshots && test.screenshots.length > 0
1851
+ ? `
1804
1852
  <div class="attachments-section">
1805
1853
  <h4>Screenshots</h4>
1806
1854
  <div class="attachments-grid">
1807
1855
  ${test.screenshots
1808
- .map(
1809
- (screenshot, index) => `
1856
+ .map(
1857
+ (screenshot, index) => `
1810
1858
  <div class="attachment-item">
1811
- <img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
1859
+ <img src="${fixPath(screenshot)}" alt="Screenshot ${
1860
+ index + 1
1861
+ }">
1812
1862
  <div class="attachment-info">
1813
1863
  <div class="trace-actions">
1814
- <a href="${fixPath(screenshot)}" target="_blank" class="view-full">View Full Image</a>
1815
- <a href="${fixPath(screenshot)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
1864
+ <a href="${fixPath(
1865
+ screenshot
1866
+ )}" target="_blank" class="view-full">View Full Image</a>
1867
+ <a href="${fixPath(
1868
+ screenshot
1869
+ )}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
1816
1870
  </div>
1817
1871
  </div>
1818
1872
  </div>
1819
1873
  `
1820
- )
1821
- .join("")}
1874
+ )
1875
+ .join("")}
1822
1876
  </div>
1823
1877
  </div>
1824
1878
  `
1825
- : ""
1879
+ : ""
1826
1880
  }
1827
- ${test.videoPath && test.videoPath.length > 0
1828
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1829
- .map((videoUrl, index) => {
1830
- const fixedVideoUrl = fixPath(videoUrl);
1831
- const fileExtension = String(fixedVideoUrl)
1832
- .split(".")
1833
- .pop()
1834
- .toLowerCase();
1835
- const mimeType =
1836
- {
1837
- mp4: "video/mp4",
1838
- webm: "video/webm",
1839
- ogg: "video/ogg",
1840
- mov: "video/quicktime",
1841
- avi: "video/x-msvideo",
1842
- }[fileExtension] || "video/mp4";
1843
- return `<div class="attachment-item video-item">
1844
- <video controls width="100%" height="auto" title="Video ${index + 1
1845
- }">
1881
+ ${
1882
+ test.videoPath && test.videoPath.length > 0
1883
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1884
+ .map((videoUrl, index) => {
1885
+ const fixedVideoUrl = fixPath(videoUrl);
1886
+ const fileExtension = String(fixedVideoUrl)
1887
+ .split(".")
1888
+ .pop()
1889
+ .toLowerCase();
1890
+ const mimeType =
1891
+ {
1892
+ mp4: "video/mp4",
1893
+ webm: "video/webm",
1894
+ ogg: "video/ogg",
1895
+ mov: "video/quicktime",
1896
+ avi: "video/x-msvideo",
1897
+ }[fileExtension] || "video/mp4";
1898
+ return `<div class="attachment-item video-item">
1899
+ <video controls width="100%" height="auto" title="Video ${
1900
+ index + 1
1901
+ }">
1846
1902
  <source src="${sanitizeHTML(
1847
- fixedVideoUrl
1848
- )}" type="${mimeType}">
1903
+ fixedVideoUrl
1904
+ )}" type="${mimeType}">
1849
1905
  Your browser does not support the video tag.
1850
1906
  </video>
1851
1907
  <div class="attachment-info">
1852
1908
  <div class="trace-actions">
1853
1909
  <a href="${sanitizeHTML(
1854
- fixedVideoUrl
1855
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1910
+ fixedVideoUrl
1911
+ )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1856
1912
  </div>
1857
1913
  </div>
1858
1914
  </div>`;
1859
- })
1860
- .join("")}</div></div>`
1861
- : ""
1915
+ })
1916
+ .join("")}</div></div>`
1917
+ : ""
1862
1918
  }
1863
- ${test.tracePath
1864
- ? `
1919
+ ${
1920
+ test.tracePath
1921
+ ? `
1865
1922
  <div class="attachments-section">
1866
1923
  <h4>Trace Files</h4>
1867
1924
  <div class="attachments-grid">
@@ -1869,70 +1926,72 @@ function generateHTML(reportData, trendData = null) {
1869
1926
  <div class="trace-preview">
1870
1927
  <span class="trace-icon">📄</span>
1871
1928
  <span class="trace-name">${sanitizeHTML(
1872
- path.basename(test.tracePath)
1873
- )}</span>
1929
+ path.basename(test.tracePath)
1930
+ )}</span>
1874
1931
  </div>
1875
1932
  <div class="attachment-info">
1876
1933
  <div class="trace-actions">
1877
1934
  <a href="${sanitizeHTML(
1878
- fixPath(test.tracePath)
1879
- )}" target="_blank" download="${sanitizeHTML(
1880
- path.basename(test.tracePath)
1881
- )}" class="download-trace">Download Trace</a>
1935
+ fixPath(test.tracePath)
1936
+ )}" target="_blank" download="${sanitizeHTML(
1937
+ path.basename(test.tracePath)
1938
+ )}" class="download-trace">Download Trace</a>
1882
1939
  </div>
1883
1940
  </div>
1884
1941
  </div>
1885
1942
  </div>
1886
1943
  </div>
1887
1944
  `
1888
- : ""
1945
+ : ""
1889
1946
  }
1890
- ${test.attachments && test.attachments.length > 0
1891
- ? `
1947
+ ${
1948
+ test.attachments && test.attachments.length > 0
1949
+ ? `
1892
1950
  <div class="attachments-section">
1893
1951
  <h4>Other Attachments</h4>
1894
1952
  <div class="attachments-grid">
1895
1953
  ${test.attachments
1896
- .map(
1897
- (attachment) => `
1954
+ .map(
1955
+ (attachment) => `
1898
1956
  <div class="attachment-item generic-attachment">
1899
1957
  <div class="attachment-icon">${getAttachmentIcon(
1900
- attachment.contentType
1901
- )}</div>
1958
+ attachment.contentType
1959
+ )}</div>
1902
1960
  <div class="attachment-caption">
1903
1961
  <span class="attachment-name" title="${sanitizeHTML(
1904
- attachment.name
1905
- )}">${sanitizeHTML(attachment.name)}</span>
1962
+ attachment.name
1963
+ )}">${sanitizeHTML(attachment.name)}</span>
1906
1964
  <span class="attachment-type">${sanitizeHTML(
1907
- attachment.contentType
1908
- )}</span>
1965
+ attachment.contentType
1966
+ )}</span>
1909
1967
  </div>
1910
1968
  <div class="attachment-info">
1911
1969
  <div class="trace-actions">
1912
1970
  <a href="${sanitizeHTML(
1913
- fixPath(attachment.path)
1914
- )}" target="_blank" class="view-full">View</a>
1971
+ fixPath(attachment.path)
1972
+ )}" target="_blank" class="view-full">View</a>
1915
1973
  <a href="${sanitizeHTML(
1916
- fixPath(attachment.path)
1917
- )}" target="_blank" download="${sanitizeHTML(
1918
- attachment.name
1919
- )}" class="download-trace">Download</a>
1974
+ fixPath(attachment.path)
1975
+ )}" target="_blank" download="${sanitizeHTML(
1976
+ attachment.name
1977
+ )}" class="download-trace">Download</a>
1920
1978
  </div>
1921
1979
  </div>
1922
1980
  </div>
1923
1981
  `
1924
- )
1925
- .join("")}
1982
+ )
1983
+ .join("")}
1926
1984
  </div>
1927
1985
  </div>
1928
1986
  `
1929
- : ""
1987
+ : ""
1930
1988
  }
1931
- ${test.codeSnippet
1932
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1933
- sanitizeHTML(test.codeSnippet)
1934
- )}</code></pre></div>`
1935
- : ""
1989
+ ${
1990
+ test.codeSnippet
1991
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1992
+ sanitizeHTML(test.codeSnippet)
1993
+ )}</code></pre></div>`
1994
+ : ""
1936
1995
  }
1937
1996
  </div>
1938
1997
  </div>`;
@@ -2345,8 +2404,8 @@ function generateHTML(reportData, trendData = null) {
2345
2404
  <h1>Playwright Pulse Report</h1>
2346
2405
  </div>
2347
2406
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2348
- runSummary.timestamp
2349
- )}<br><strong>Total Duration:</strong> ${formatDuration(
2407
+ runSummary.timestamp
2408
+ )}<br><strong>Total Duration:</strong> ${formatDuration(
2350
2409
  runSummary.duration
2351
2410
  )}</div>
2352
2411
  </header>
@@ -2358,35 +2417,40 @@ function generateHTML(reportData, trendData = null) {
2358
2417
  </div>
2359
2418
  <div id="dashboard" class="tab-content active">
2360
2419
  <div class="dashboard-grid">
2361
- <div class="summary-card"><h3>Total Tests</h3><div class="value">${runSummary.totalTests
2362
- }</div></div>
2363
- <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${runSummary.passed
2364
- }</div><div class="trend-percentage">${passPercentage}%</div></div>
2365
- <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${runSummary.failed
2366
- }</div><div class="trend-percentage">${failPercentage}%</div></div>
2367
- <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${runSummary.skipped || 0
2368
- }</div><div class="trend-percentage">${skipPercentage}%</div></div>
2420
+ <div class="summary-card"><h3>Total Tests</h3><div class="value">${
2421
+ runSummary.totalTests
2422
+ }</div></div>
2423
+ <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
2424
+ runSummary.passed
2425
+ }</div><div class="trend-percentage">${passPercentage}%</div></div>
2426
+ <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
2427
+ runSummary.failed
2428
+ }</div><div class="trend-percentage">${failPercentage}%</div></div>
2429
+ <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
2430
+ runSummary.skipped || 0
2431
+ }</div><div class="trend-percentage">${skipPercentage}%</div></div>
2369
2432
  <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
2370
2433
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
2371
- runSummary.duration
2372
- )}</div></div>
2434
+ runSummary.duration
2435
+ )}</div></div>
2373
2436
  </div>
2374
2437
  <div class="dashboard-bottom-row">
2375
2438
  <div style="display: grid; gap: 20px">
2376
2439
  ${generatePieChart(
2377
- [
2378
- { label: "Passed", value: runSummary.passed },
2379
- { label: "Failed", value: runSummary.failed },
2380
- { label: "Skipped", value: runSummary.skipped || 0 },
2381
- ],
2382
- 400,
2383
- 390
2384
- )}
2385
- ${runSummary.environment &&
2386
- Object.keys(runSummary.environment).length > 0
2387
- ? generateEnvironmentDashboard(runSummary.environment)
2388
- : '<div class="no-data">Environment data not available.</div>'
2389
- }
2440
+ [
2441
+ { label: "Passed", value: runSummary.passed },
2442
+ { label: "Failed", value: runSummary.failed },
2443
+ { label: "Skipped", value: runSummary.skipped || 0 },
2444
+ ],
2445
+ 400,
2446
+ 390
2447
+ )}
2448
+ ${
2449
+ runSummary.environment &&
2450
+ Object.keys(runSummary.environment).length > 0
2451
+ ? generateEnvironmentDashboard(runSummary.environment)
2452
+ : '<div class="no-data">Environment data not available.</div>'
2453
+ }
2390
2454
  </div>
2391
2455
  ${generateSuitesWidget(suitesData)}
2392
2456
  </div>
@@ -2396,17 +2460,17 @@ function generateHTML(reportData, trendData = null) {
2396
2460
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
2397
2461
  <select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
2398
2462
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
2399
- new Set(
2400
- (results || []).map((test) => test.browser || "unknown")
2401
- )
2402
- )
2403
- .map(
2404
- (browser) =>
2405
- `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2406
- browser
2407
- )}</option>`
2408
- )
2409
- .join("")}</select>
2463
+ new Set(
2464
+ (results || []).map((test) => test.browser || "unknown")
2465
+ )
2466
+ )
2467
+ .map(
2468
+ (browser) =>
2469
+ `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2470
+ browser
2471
+ )}</option>`
2472
+ )
2473
+ .join("")}</select>
2410
2474
  <button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
2411
2475
  </div>
2412
2476
  <div class="test-cases-list">${generateTestCasesHTML()}</div>
@@ -2415,16 +2479,18 @@ function generateHTML(reportData, trendData = null) {
2415
2479
  <h2 class="tab-main-title">Execution Trends</h2>
2416
2480
  <div class="trend-charts-row">
2417
2481
  <div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
2418
- ${trendData && trendData.overall && trendData.overall.length > 0
2419
- ? generateTestTrendsChart(trendData)
2420
- : '<div class="no-data">Overall trend data not available for test counts.</div>'
2421
- }
2482
+ ${
2483
+ trendData && trendData.overall && trendData.overall.length > 0
2484
+ ? generateTestTrendsChart(trendData)
2485
+ : '<div class="no-data">Overall trend data not available for test counts.</div>'
2486
+ }
2422
2487
  </div>
2423
2488
  <div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
2424
- ${trendData && trendData.overall && trendData.overall.length > 0
2425
- ? generateDurationTrendChart(trendData)
2426
- : '<div class="no-data">Overall trend data not available for durations.</div>'
2427
- }
2489
+ ${
2490
+ trendData && trendData.overall && trendData.overall.length > 0
2491
+ ? generateDurationTrendChart(trendData)
2492
+ : '<div class="no-data">Overall trend data not available for durations.</div>'
2493
+ }
2428
2494
  </div>
2429
2495
  </div>
2430
2496
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
@@ -2434,12 +2500,13 @@ function generateHTML(reportData, trendData = null) {
2434
2500
  </div>
2435
2501
  </div>
2436
2502
  <h2 class="tab-main-title">Individual Test History</h2>
2437
- ${trendData &&
2438
- trendData.testRuns &&
2439
- Object.keys(trendData.testRuns).length > 0
2440
- ? generateTestHistoryContent(trendData)
2441
- : '<div class="no-data">Individual test history data not available.</div>'
2442
- }
2503
+ ${
2504
+ trendData &&
2505
+ trendData.testRuns &&
2506
+ Object.keys(trendData.testRuns).length > 0
2507
+ ? generateTestHistoryContent(trendData)
2508
+ : '<div class="no-data">Individual test history data not available.</div>'
2509
+ }
2443
2510
  </div>
2444
2511
  <div id="ai-failure-analyzer" class="tab-content">
2445
2512
  ${generateAIFailureAnalyzerTab(results)}
@@ -2702,6 +2769,19 @@ function getAIFix(button) {
2702
2769
  }
2703
2770
  if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
2704
2771
  if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
2772
+ // --- Annotation Link Handler ---
2773
+ document.querySelectorAll('a.annotation-link').forEach(link => {
2774
+ link.addEventListener('click', (e) => {
2775
+ e.preventDefault();
2776
+ const annotationId = link.dataset.annotation;
2777
+ if (annotationId) {
2778
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
2779
+ if (jiraUrl) {
2780
+ window.open(jiraUrl + annotationId, '_blank');
2781
+ }
2782
+ }
2783
+ });
2784
+ });
2705
2785
  // --- Intersection Observer for Lazy Loading ---
2706
2786
  const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
2707
2787
  if ('IntersectionObserver' in window) {
@@ -1939,18 +1939,66 @@ function generateHTML(reportData, trendData = null) {
1939
1939
  <p><strong>Full Path:</strong> ${sanitizeHTML(
1940
1940
  test.name
1941
1941
  )}</p>
1942
+ ${
1943
+ test.annotations && test.annotations.length > 0
1944
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
1945
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
1946
+ ${test.annotations
1947
+ .map((annotation, idx) => {
1948
+ const isIssueOrBug =
1949
+ annotation.type === "issue" ||
1950
+ annotation.type === "bug";
1951
+ const descriptionText =
1952
+ annotation.description || "";
1953
+ const typeLabel = sanitizeHTML(
1954
+ annotation.type
1955
+ );
1956
+ const descriptionHtml =
1957
+ isIssueOrBug &&
1958
+ descriptionText.match(/^[A-Z]+-\d+$/)
1959
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
1960
+ descriptionText
1961
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
1962
+ descriptionText
1963
+ )}</a>`
1964
+ : sanitizeHTML(descriptionText);
1965
+ const locationText = annotation.location
1966
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
1967
+ annotation.location.file
1968
+ )}:${annotation.location.line}:${
1969
+ annotation.location.column
1970
+ }</div>`
1971
+ : "";
1972
+ return `<div style="margin-bottom: ${
1973
+ idx < test.annotations.length - 1
1974
+ ? "10px"
1975
+ : "0"
1976
+ };">
1977
+ <strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
1978
+ ${
1979
+ descriptionText
1980
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
1981
+ : ""
1982
+ }
1983
+ ${locationText}
1984
+ </div>`;
1985
+ })
1986
+ .join("")}
1987
+ </div>`
1988
+ : ""
1989
+ }
1942
1990
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1943
1991
  test.workerId
1944
1992
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1945
1993
  test.totalWorkers
1946
1994
  )}]</p>
1947
- ${
1948
- test.errorMessage
1949
- ? `<div class="test-error-summary">${formatPlaywrightError(
1950
- test.errorMessage
1951
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1952
- : ""
1953
- }
1995
+ ${
1996
+ test.errorMessage
1997
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1998
+ test.errorMessage
1999
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
2000
+ : ""
2001
+ }
1954
2002
  ${
1955
2003
  test.snippet
1956
2004
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
@@ -1998,7 +2046,7 @@ function generateHTML(reportData, trendData = null) {
1998
2046
  test.screenshots.length === 0
1999
2047
  )
2000
2048
  return "";
2001
- return `<div class="attachments-section"><h4>Screenshots (Click to load Images)</h4><div class="attachments-grid">${test.screenshots
2049
+ return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
2002
2050
  .map((screenshotPath, index) => {
2003
2051
  try {
2004
2052
  const imagePath = path.resolve(
@@ -2840,6 +2888,18 @@ aspect-ratio: 16 / 9;
2840
2888
  }
2841
2889
  return;
2842
2890
  }
2891
+ const annotationLink = e.target.closest('a.annotation-link');
2892
+ if (annotationLink) {
2893
+ e.preventDefault();
2894
+ const annotationId = annotationLink.dataset.annotation;
2895
+ if (annotationId) {
2896
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
2897
+ if (jiraUrl) {
2898
+ window.open(jiraUrl + annotationId, '_blank');
2899
+ }
2900
+ }
2901
+ return;
2902
+ }
2843
2903
  const img = e.target.closest('img.lazy-load-image');
2844
2904
  if (img && img.dataset && img.dataset.src) {
2845
2905
  if (e.preventDefault) e.preventDefault();
@@ -2870,9 +2930,45 @@ aspect-ratio: 16 / 9;
2870
2930
  const a = e.target.closest('a.lazy-load-attachment');
2871
2931
  if (a && a.dataset && a.dataset.href) {
2872
2932
  e.preventDefault();
2873
- a.href = a.dataset.href;
2874
- a.removeAttribute('data-href');
2875
- a.click();
2933
+
2934
+ // Special handling for view-full links to avoid about:blank issue
2935
+ if (a.classList.contains('view-full')) {
2936
+ // Extract the data from the data URI
2937
+ const dataUri = a.dataset.href;
2938
+ const [header, base64Data] = dataUri.split(',');
2939
+ const mimeType = header.match(/data:([^;]+)/)[1];
2940
+
2941
+ try {
2942
+ // Convert base64 to blob
2943
+ const byteCharacters = atob(base64Data);
2944
+ const byteNumbers = new Array(byteCharacters.length);
2945
+ for (let i = 0; i < byteCharacters.length; i++) {
2946
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
2947
+ }
2948
+ const byteArray = new Uint8Array(byteNumbers);
2949
+ const blob = new Blob([byteArray], { type: mimeType });
2950
+
2951
+ // Create a URL and open it
2952
+ const blobUrl = URL.createObjectURL(blob);
2953
+ const newWindow = window.open(blobUrl, '_blank');
2954
+
2955
+ // Clean up the URL after a delay
2956
+ setTimeout(() => {
2957
+ URL.revokeObjectURL(blobUrl);
2958
+ }, 1000);
2959
+ } catch (error) {
2960
+ console.error('Failed to open attachment:', error);
2961
+ // Fallback to original method
2962
+ a.href = a.dataset.href;
2963
+ a.removeAttribute('data-href');
2964
+ a.click();
2965
+ }
2966
+ } else {
2967
+ // For download links, use the original method
2968
+ a.href = a.dataset.href;
2969
+ a.removeAttribute('data-href');
2970
+ a.click();
2971
+ }
2876
2972
  return;
2877
2973
  }
2878
2974
  });
@@ -289,7 +289,7 @@ const sendEmail = async (credentials) => {
289
289
  }
290
290
  };
291
291
 
292
- async function fetchCredentials(retries = 6) {
292
+ async function fetchCredentials(retries = 10) {
293
293
  // Ensure fetch is initialized from the dynamic import before calling this
294
294
  if (!fetch) {
295
295
  try {
@@ -324,7 +324,7 @@ async function fetchCredentials(retries = 6) {
324
324
  });
325
325
 
326
326
  const fetchPromise = fetch(
327
- "https://test-dashboard-66zd.onrender.com/api/getcredentials",
327
+ "https://get-credentials.netlify.app/api/getcredentials",
328
328
  {
329
329
  method: "GET",
330
330
  headers: {