@arghajit/dummy 0.1.2 → 0.1.3

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
@@ -16,7 +16,7 @@ The project provides these utility commands:
16
16
  | Command | Description |
17
17
  |------------------------|-----------------------------------------------------------------------------|
18
18
  | `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 |
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, with a dark theme and better initial load handling |
20
20
  | `merge-pulse-report` | Combines multiple parallel test json reports, basically used in sharding |
21
21
  | `generate-trend` | Analyzes historical trends in test results |
22
22
  | `generate-email-report`| Generates email-friendly report versions |
@@ -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);
@@ -212,15 +200,13 @@ class PlaywrightPulseReporter {
212
200
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
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,
@@ -284,8 +267,7 @@ class PlaywrightPulseReporter {
284
267
  if (!attachment.path)
285
268
  continue;
286
269
  try {
287
- // Use the new testIdWithRunCounter for the subfolder
288
- const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
270
+ const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
289
271
  const safeAttachmentName = path
290
272
  .basename(attachment.path)
291
273
  .replace(/[^a-zA-Z0-9_.-]/g, "_");
@@ -320,46 +302,14 @@ class PlaywrightPulseReporter {
320
302
  _getFinalizedResults(allResults) {
321
303
  const finalResultsMap = new Map();
322
304
  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
- }
305
+ const existing = finalResultsMap.get(result.id);
306
+ // Keep the result with the highest retry attempt for each test ID
307
+ if (!existing || result.retries >= existing.retries) {
308
+ finalResultsMap.set(result.id, result);
345
309
  }
346
310
  }
347
311
  return Array.from(finalResultsMap.values());
348
312
  }
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
313
  onError(error) {
364
314
  var _a;
365
315
  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 +344,15 @@ class PlaywrightPulseReporter {
394
344
  }
395
345
  }
396
346
  async _mergeShardResults(finalRunData) {
397
- let allShardRawResults = []; // Store raw results before final de-duplication
347
+ let allShardProcessedResults = [];
398
348
  const totalShards = this.config.shard ? this.config.shard.total : 1;
399
349
  for (let i = 0; i < totalShards; i++) {
400
350
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
401
351
  try {
402
352
  const content = await fs.readFile(tempFilePath, "utf-8");
403
353
  const shardResults = JSON.parse(content);
404
- allShardRawResults = allShardRawResults.concat(shardResults);
354
+ allShardProcessedResults =
355
+ allShardProcessedResults.concat(shardResults);
405
356
  }
406
357
  catch (error) {
407
358
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
@@ -412,14 +363,11 @@ class PlaywrightPulseReporter {
412
363
  }
413
364
  }
414
365
  }
415
- // Apply _getFinalizedResults after all raw shard results are collected
416
- const finalResultsList = this._getFinalizedResults(allShardRawResults);
366
+ const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
417
367
  finalResultsList.forEach((r) => (r.runId = finalRunData.id));
418
368
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
419
369
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
420
370
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
421
- // Add flaky count
422
- finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
423
371
  finalRunData.totalTests = finalResultsList.length;
424
372
  const reviveDates = (key, value) => {
425
373
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -466,36 +414,34 @@ class PlaywrightPulseReporter {
466
414
  await this._writeShardResults();
467
415
  return;
468
416
  }
469
- // `this.results` now contains all individual run attempts.
470
- // _getFinalizedResults will select the "best" run for each logical test.
417
+ // De-duplicate and handle retries here, in a safe, single-threaded context.
471
418
  const finalResults = this._getFinalizedResults(this.results);
472
419
  const runEndTime = Date.now();
473
420
  const duration = runEndTime - this.runStartTime;
474
- // Use the stored overall runId
475
- const runId = this.currentRunId;
421
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
476
422
  const environmentDetails = this._getEnvDetails();
477
423
  const runData = {
478
424
  id: runId,
479
425
  timestamp: new Date(this.runStartTime),
426
+ // Use the length of the de-duplicated array for all counts
480
427
  totalTests: finalResults.length,
481
428
  passed: finalResults.filter((r) => r.status === "passed").length,
482
429
  failed: finalResults.filter((r) => r.status === "failed").length,
483
430
  skipped: finalResults.filter((r) => r.status === "skipped").length,
484
- flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
485
431
  duration,
486
432
  environment: environmentDetails,
487
433
  };
488
- // Ensure all final results have the correct overall runId
489
434
  finalResults.forEach((r) => (r.runId = runId));
490
435
  let finalReport = undefined;
491
436
  if (this.isSharded) {
492
- // _mergeShardResults will now perform the final de-duplication across shards
437
+ // The _mergeShardResults method will handle its own de-duplication
493
438
  finalReport = await this._mergeShardResults(runData);
494
439
  }
495
440
  else {
496
441
  finalReport = {
497
442
  run: runData,
498
- results: finalResults, // Use the de-duplicated results for a non-sharded run
443
+ // Use the de-duplicated results
444
+ results: finalResults,
499
445
  metadata: { generatedAt: new Date().toISOString() },
500
446
  };
501
447
  }
@@ -524,6 +470,7 @@ class PlaywrightPulseReporter {
524
470
  }
525
471
  }
526
472
  else {
473
+ // Logic for appending/merging reports
527
474
  const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
528
475
  const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
529
476
  try {
@@ -569,10 +516,7 @@ class PlaywrightPulseReporter {
569
516
  const allResultsFromAllFiles = [];
570
517
  let latestTimestamp = new Date(0);
571
518
  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;
519
+ let totalDuration = 0;
576
520
  for (const file of reportFiles) {
577
521
  const filePath = path.join(pulseResultsDir, file);
578
522
  try {
@@ -595,28 +539,22 @@ class PlaywrightPulseReporter {
595
539
  }
596
540
  // De-duplicate the results from ALL merged files using the helper function
597
541
  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;
542
+ // Sum the duration from the final, de-duplicated list of tests
543
+ totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
606
544
  const combinedRun = {
607
545
  id: `merged-${Date.now()}`,
608
546
  timestamp: latestTimestamp,
609
547
  environment: lastRunEnvironment,
548
+ // Recalculate counts based on the truly final, de-duplicated list
610
549
  totalTests: finalMergedResults.length,
611
550
  passed: finalMergedResults.filter((r) => r.status === "passed").length,
612
551
  failed: finalMergedResults.filter((r) => r.status === "failed").length,
613
552
  skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
614
- flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
615
553
  duration: totalDuration,
616
554
  };
617
555
  const finalReport = {
618
556
  run: combinedRun,
619
- results: finalMergedResults,
557
+ results: finalMergedResults, // Use the de-duplicated list
620
558
  metadata: {
621
559
  generatedAt: new Date().toISOString(),
622
560
  },
@@ -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;
@@ -56,7 +54,6 @@ export interface TestRun {
56
54
  passed: number;
57
55
  failed: number;
58
56
  skipped: number;
59
- flaky: number;
60
57
  duration: number;
61
58
  environment?: EnvDetails;
62
59
  }
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.1.3",
4
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
5
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
6
7
  "keywords": [
@@ -72,13 +73,16 @@
72
73
  "devDependencies": {
73
74
  "@types/node": "^20",
74
75
  "@types/ua-parser-js": "^0.7.39",
75
- "eslint": "9.25.1",
76
+ "eslint": "^9.39.1",
76
77
  "typescript": "^5"
77
78
  },
78
79
  "engines": {
79
- "node": ">=16"
80
+ "node": ">=18"
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@playwright/test": ">=1.40.0"
84
+ },
85
+ "overrides": {
86
+ "glob": "^13.0.0"
83
87
  }
84
88
  }
@@ -1850,53 +1850,43 @@ function generateHTML(reportData, trendData = null) {
1850
1850
  : ""
1851
1851
  }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1852
1852
  : ""
1853
- }${(() => {
1854
- if (!step.attachments || step.attachments.length === 0)
1855
- return "";
1856
- return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1857
- .map((attachment) => {
1858
- try {
1859
- const attachmentPath = path.resolve(
1860
- DEFAULT_OUTPUT_DIR,
1861
- attachment.path
1862
- );
1863
- if (!fsExistsSync(attachmentPath)) {
1864
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1865
- attachment.name
1866
- )}</div>`;
1867
- }
1868
- const attachmentBase64 =
1869
- readFileSync(attachmentPath).toString("base64");
1870
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1871
- return `<div class="attachment-item generic-attachment">
1872
- <div class="attachment-icon">${getAttachmentIcon(
1873
- attachment.contentType
1874
- )}</div>
1853
+ }${
1854
+ (() => {
1855
+ if (!step.attachments || step.attachments.length === 0) return "";
1856
+ return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1857
+ .map((attachment) => {
1858
+ try {
1859
+ const attachmentPath = path.resolve(
1860
+ DEFAULT_OUTPUT_DIR,
1861
+ attachment.path
1862
+ );
1863
+ if (!fsExistsSync(attachmentPath)) {
1864
+ return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1865
+ attachment.name
1866
+ )}</div>`;
1867
+ }
1868
+ const attachmentBase64 = readFileSync(attachmentPath).toString("base64");
1869
+ const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1870
+ return `<div class="attachment-item generic-attachment">
1871
+ <div class="attachment-icon">${getAttachmentIcon(attachment.contentType)}</div>
1875
1872
  <div class="attachment-caption">
1876
- <span class="attachment-name" title="${sanitizeHTML(
1877
- attachment.name
1878
- )}">${sanitizeHTML(attachment.name)}</span>
1879
- <span class="attachment-type">${sanitizeHTML(
1880
- attachment.contentType
1881
- )}</span>
1873
+ <span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
1874
+ <span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
1882
1875
  </div>
1883
1876
  <div class="attachment-info">
1884
1877
  <div class="trace-actions">
1885
1878
  <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
1886
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1887
- attachment.name
1888
- )}">Download</a>
1879
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
1889
1880
  </div>
1890
1881
  </div>
1891
1882
  </div>`;
1892
- } catch (e) {
1893
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
1894
- attachment.name
1895
- )}</div>`;
1896
- }
1897
- })
1898
- .join("")}</div></div>`;
1899
- })()}${
1883
+ } catch (e) {
1884
+ return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(attachment.name)}</div>`;
1885
+ }
1886
+ })
1887
+ .join("")}</div></div>`;
1888
+ })()
1889
+ }${
1900
1890
  hasNestedSteps
1901
1891
  ? `<div class="nested-steps">${generateStepsHTML(
1902
1892
  step.steps,
@@ -1913,9 +1903,7 @@ function generateHTML(reportData, trendData = null) {
1913
1903
  test.tags || []
1914
1904
  )
1915
1905
  .join(",")
1916
- .toLowerCase()}" data-test-id="${sanitizeHTML(
1917
- String(test.id || testIndex)
1918
- )}">
1906
+ .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1919
1907
  <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1920
1908
  test.status
1921
1909
  )}">${String(
@@ -1982,9 +1970,7 @@ function generateHTML(reportData, trendData = null) {
1982
1970
  ${
1983
1971
  test.stderr && test.stderr.length > 0
1984
1972
  ? (() => {
1985
- const logId = `stderr-log-${
1986
- test.id || testIndex
1987
- }`;
1973
+ const logId = `stderr-log-${test.id || testIndex}`;
1988
1974
  return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1989
1975
  .map((line) => sanitizeHTML(line))
1990
1976
  .join("\\n")}</pre></div>`;
@@ -1998,7 +1984,7 @@ function generateHTML(reportData, trendData = null) {
1998
1984
  test.screenshots.length === 0
1999
1985
  )
2000
1986
  return "";
2001
- return `<div class="attachments-section"><h4>Screenshots (Click to load Images)</h4><div class="attachments-grid">${test.screenshots
1987
+ return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
2002
1988
  .map((screenshotPath, index) => {
2003
1989
  try {
2004
1990
  const imagePath = path.resolve(
@@ -2398,7 +2384,7 @@ aspect-ratio: 16 / 9;
2398
2384
  @media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
2399
2385
  @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
2400
2386
  @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
2401
- .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2387
+ .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
2402
2388
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
2403
2389
  .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2404
2390
  .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }