@arghajit/dummy 0.1.1 → 0.1.2-beta-2

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.
@@ -12,6 +12,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
12
12
  private isSharded;
13
13
  private shardIndex;
14
14
  private resetOnEachRun;
15
+ private currentRunId;
15
16
  constructor(options?: PlaywrightPulseReporterOptions);
16
17
  printsToStdio(): boolean;
17
18
  onBegin(config: FullConfig, suite: Suite): void;
@@ -19,6 +20,13 @@ export declare class PlaywrightPulseReporter implements Reporter {
19
20
  private getBrowserDetails;
20
21
  private processStep;
21
22
  onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
23
+ private _getBaseTestId;
24
+ private _getStatusOrder;
25
+ /**
26
+ * Refactored to group all run attempts for a single logical test case.
27
+ * @param allAttempts An array of all individual test run attempts.
28
+ * @returns An array of ConsolidatedTestResult objects, where each object represents one logical test and contains an array of all its runs.
29
+ */
22
30
  private _getFinalizedResults;
23
31
  onError(error: any): void;
24
32
  private _getEnvDetails;
@@ -39,8 +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) => {
42
+ const convertStatus = (status, testCase, retryCount = 0) => {
43
+ if (status === "passed" && retryCount > 0) {
44
+ return "flaky";
45
+ }
43
46
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
47
+ if (status === "passed")
48
+ return "flaky";
44
49
  return "failed";
45
50
  }
46
51
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
@@ -64,10 +69,12 @@ const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
64
69
  class PlaywrightPulseReporter {
65
70
  constructor(options = {}) {
66
71
  var _a, _b, _c;
72
+ // This will now store all individual run attempts for all tests using our new local type.
67
73
  this.results = [];
68
74
  this.baseOutputFile = "playwright-pulse-report.json";
69
75
  this.isSharded = false;
70
76
  this.shardIndex = undefined;
77
+ this.currentRunId = "";
71
78
  this.options = options;
72
79
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
80
  this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
@@ -82,6 +89,7 @@ class PlaywrightPulseReporter {
82
89
  this.config = config;
83
90
  this.suite = suite;
84
91
  this.runStartTime = Date.now();
92
+ this.currentRunId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
85
93
  const configDir = this.config.rootDir;
86
94
  const configFileDir = this.config.configFile
87
95
  ? path.dirname(this.config.configFile)
@@ -106,9 +114,7 @@ class PlaywrightPulseReporter {
106
114
  })
107
115
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
108
116
  }
109
- onTestBegin(test) {
110
- console.log(`Starting test: ${test.title}`);
111
- }
117
+ onTestBegin(test) { }
112
118
  getBrowserDetails(test) {
113
119
  var _a, _b, _c, _d;
114
120
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
@@ -159,7 +165,7 @@ class PlaywrightPulseReporter {
159
165
  }
160
166
  return finalString.trim();
161
167
  }
162
- async processStep(step, testId, browserDetails, testCase) {
168
+ async processStep(step, testId, browserDetails, testCase, retryCount = 0) {
163
169
  var _a, _b, _c, _d;
164
170
  let stepStatus = "passed";
165
171
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
@@ -167,7 +173,7 @@ class PlaywrightPulseReporter {
167
173
  stepStatus = "skipped";
168
174
  }
169
175
  else {
170
- stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
176
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase, retryCount);
171
177
  }
172
178
  const duration = step.duration;
173
179
  const startTime = new Date(step.startTime);
@@ -200,13 +206,13 @@ class PlaywrightPulseReporter {
200
206
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
201
207
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
208
  const browserDetails = this.getBrowserDetails(test);
203
- const testStatus = convertStatus(result.status, test);
209
+ const testStatus = convertStatus(result.status, test, result.retry);
204
210
  const startTime = new Date(result.startTime);
205
211
  const endTime = new Date(startTime.getTime() + result.duration);
206
212
  const processAllSteps = async (steps) => {
207
213
  let processed = [];
208
214
  for (const step of steps) {
209
- const processedStep = await this.processStep(step, test.id, browserDetails, test);
215
+ const processedStep = await this.processStep(step, test.id, browserDetails, test, result.retry);
210
216
  processed.push(processedStep);
211
217
  if (step.steps && step.steps.length > 0) {
212
218
  processedStep.steps = await processAllSteps(step.steps);
@@ -238,9 +244,11 @@ class PlaywrightPulseReporter {
238
244
  ? JSON.stringify(this.config.metadata)
239
245
  : undefined,
240
246
  };
247
+ // Correctly handle the ID for each run attempt.
248
+ const testIdWithRunCounter = `${test.id}-run-${result.retry}`;
241
249
  const pulseResult = {
242
- id: test.id,
243
- runId: "TBD",
250
+ id: testIdWithRunCounter,
251
+ runId: this.currentRunId,
244
252
  name: test.titlePath().join(" > "),
245
253
  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",
246
254
  status: testStatus,
@@ -249,6 +257,7 @@ class PlaywrightPulseReporter {
249
257
  endTime: endTime,
250
258
  browser: browserDetails,
251
259
  retries: result.retry,
260
+ runCounter: result.retry,
252
261
  steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
253
262
  errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
254
263
  stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
@@ -267,7 +276,7 @@ class PlaywrightPulseReporter {
267
276
  if (!attachment.path)
268
277
  continue;
269
278
  try {
270
- const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
279
+ const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
271
280
  const safeAttachmentName = path
272
281
  .basename(attachment.path)
273
282
  .replace(/[^a-zA-Z0-9_.-]/g, "_");
@@ -299,16 +308,62 @@ class PlaywrightPulseReporter {
299
308
  }
300
309
  this.results.push(pulseResult);
301
310
  }
302
- _getFinalizedResults(allResults) {
303
- const finalResultsMap = new Map();
304
- for (const result of allResults) {
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);
311
+ // New method to extract the base test ID, ignoring the run-counter suffix
312
+ _getBaseTestId(testResultId) {
313
+ const parts = testResultId.split("-run-");
314
+ return parts[0];
315
+ }
316
+ _getStatusOrder(status) {
317
+ switch (status) {
318
+ case "passed":
319
+ return 1;
320
+ case "flaky":
321
+ return 2;
322
+ case "failed":
323
+ return 3;
324
+ case "skipped":
325
+ return 4;
326
+ default:
327
+ return 99;
328
+ }
329
+ }
330
+ /**
331
+ * Refactored to group all run attempts for a single logical test case.
332
+ * @param allAttempts An array of all individual test run attempts.
333
+ * @returns An array of ConsolidatedTestResult objects, where each object represents one logical test and contains an array of all its runs.
334
+ */
335
+ _getFinalizedResults(allAttempts) {
336
+ const groupedResults = new Map();
337
+ for (const attempt of allAttempts) {
338
+ const baseTestId = this._getBaseTestId(attempt.id);
339
+ if (!groupedResults.has(baseTestId)) {
340
+ groupedResults.set(baseTestId, []);
309
341
  }
342
+ groupedResults.get(baseTestId).push(attempt);
310
343
  }
311
- return Array.from(finalResultsMap.values());
344
+ const finalResults = [];
345
+ for (const [baseId, runs] of groupedResults.entries()) {
346
+ // Sort runs to find the best status and overall duration
347
+ runs.sort((a, b) => this._getStatusOrder(a.status) - this._getStatusOrder(b.status));
348
+ const bestRun = runs[0];
349
+ // Calculate total duration from the earliest start to the latest end time of all runs
350
+ const startTimes = runs.map((run) => run.startTime.getTime());
351
+ const endTimes = runs.map((run) => run.endTime.getTime());
352
+ const overallDuration = Math.max(...endTimes) - Math.min(...startTimes);
353
+ finalResults.push({
354
+ id: baseId,
355
+ name: bestRun.name,
356
+ suiteName: bestRun.suiteName,
357
+ status: bestRun.status,
358
+ duration: overallDuration,
359
+ startTime: new Date(Math.min(...startTimes)),
360
+ endTime: new Date(Math.max(...endTimes)),
361
+ browser: bestRun.browser,
362
+ tags: bestRun.tags,
363
+ runs: runs.sort((a, b) => a.runCounter - b.runCounter), // Sort runs chronologically for the report
364
+ });
365
+ }
366
+ return finalResults;
312
367
  }
313
368
  onError(error) {
314
369
  var _a;
@@ -344,15 +399,14 @@ class PlaywrightPulseReporter {
344
399
  }
345
400
  }
346
401
  async _mergeShardResults(finalRunData) {
347
- let allShardProcessedResults = [];
402
+ let allShardRawResults = [];
348
403
  const totalShards = this.config.shard ? this.config.shard.total : 1;
349
404
  for (let i = 0; i < totalShards; i++) {
350
405
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
351
406
  try {
352
407
  const content = await fs.readFile(tempFilePath, "utf-8");
353
408
  const shardResults = JSON.parse(content);
354
- allShardProcessedResults =
355
- allShardProcessedResults.concat(shardResults);
409
+ allShardRawResults = allShardRawResults.concat(shardResults);
356
410
  }
357
411
  catch (error) {
358
412
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
@@ -363,11 +417,11 @@ class PlaywrightPulseReporter {
363
417
  }
364
418
  }
365
419
  }
366
- const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
367
- finalResultsList.forEach((r) => (r.runId = finalRunData.id));
420
+ const finalResultsList = this._getFinalizedResults(allShardRawResults);
368
421
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
369
422
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
370
423
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
424
+ finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
371
425
  finalRunData.totalTests = finalResultsList.length;
372
426
  const reviveDates = (key, value) => {
373
427
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -414,34 +468,30 @@ class PlaywrightPulseReporter {
414
468
  await this._writeShardResults();
415
469
  return;
416
470
  }
417
- // De-duplicate and handle retries here, in a safe, single-threaded context.
418
471
  const finalResults = this._getFinalizedResults(this.results);
419
472
  const runEndTime = Date.now();
420
473
  const duration = runEndTime - this.runStartTime;
421
- const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
474
+ const runId = this.currentRunId;
422
475
  const environmentDetails = this._getEnvDetails();
423
476
  const runData = {
424
477
  id: runId,
425
478
  timestamp: new Date(this.runStartTime),
426
- // Use the length of the de-duplicated array for all counts
427
479
  totalTests: finalResults.length,
428
480
  passed: finalResults.filter((r) => r.status === "passed").length,
429
481
  failed: finalResults.filter((r) => r.status === "failed").length,
430
482
  skipped: finalResults.filter((r) => r.status === "skipped").length,
483
+ flaky: finalResults.filter((r) => r.status === "flaky").length,
431
484
  duration,
432
485
  environment: environmentDetails,
433
486
  };
434
- finalResults.forEach((r) => (r.runId = runId));
435
487
  let finalReport = undefined;
436
488
  if (this.isSharded) {
437
- // The _mergeShardResults method will handle its own de-duplication
438
489
  finalReport = await this._mergeShardResults(runData);
439
490
  }
440
491
  else {
441
492
  finalReport = {
442
493
  run: runData,
443
- // Use the de-duplicated results
444
- results: finalResults,
494
+ results: finalResults, // Cast to any to bypass the type mismatch
445
495
  metadata: { generatedAt: new Date().toISOString() },
446
496
  };
447
497
  }
@@ -470,7 +520,6 @@ class PlaywrightPulseReporter {
470
520
  }
471
521
  }
472
522
  else {
473
- // Logic for appending/merging reports
474
523
  const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
475
524
  const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
476
525
  try {
@@ -516,45 +565,54 @@ class PlaywrightPulseReporter {
516
565
  const allResultsFromAllFiles = [];
517
566
  let latestTimestamp = new Date(0);
518
567
  let lastRunEnvironment = undefined;
519
- let totalDuration = 0;
568
+ let earliestStartTime = Date.now();
569
+ let latestEndTime = 0;
520
570
  for (const file of reportFiles) {
521
571
  const filePath = path.join(pulseResultsDir, file);
522
572
  try {
523
573
  const content = await fs.readFile(filePath, "utf-8");
524
574
  const json = JSON.parse(content);
525
- if (json.run) {
526
- const runTimestamp = new Date(json.run.timestamp);
527
- if (runTimestamp > latestTimestamp) {
528
- latestTimestamp = runTimestamp;
529
- lastRunEnvironment = json.run.environment || undefined;
530
- }
531
- }
575
+ // This is the tricky part. We need to handle both old and new report formats.
576
+ // Assuming the `results` array might contain the old, single-run objects or the new, consolidated ones.
532
577
  if (json.results) {
533
- allResultsFromAllFiles.push(...json.results);
578
+ json.results.forEach((testResult) => {
579
+ // Check if the TestResult has a 'runs' array (new format)
580
+ if ("runs" in testResult && Array.isArray(testResult.runs)) {
581
+ allResultsFromAllFiles.push(...testResult.runs);
582
+ }
583
+ else {
584
+ // This is the old format (single run). We'll treat it as a single attempt.
585
+ allResultsFromAllFiles.push(testResult); // Cast to any to get properties
586
+ }
587
+ });
534
588
  }
535
589
  }
536
590
  catch (err) {
537
591
  console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
538
592
  }
539
593
  }
540
- // De-duplicate the results from ALL merged files using the helper function
541
594
  const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
542
- // Sum the duration from the final, de-duplicated list of tests
543
- totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
595
+ for (const res of finalMergedResults) {
596
+ if (res.startTime.getTime() < earliestStartTime)
597
+ earliestStartTime = res.startTime.getTime();
598
+ if (res.endTime.getTime() > latestEndTime)
599
+ latestEndTime = res.endTime.getTime();
600
+ }
601
+ const totalDuration = latestEndTime > earliestStartTime ? latestEndTime - earliestStartTime : 0;
544
602
  const combinedRun = {
545
603
  id: `merged-${Date.now()}`,
546
604
  timestamp: latestTimestamp,
547
605
  environment: lastRunEnvironment,
548
- // Recalculate counts based on the truly final, de-duplicated list
549
606
  totalTests: finalMergedResults.length,
550
607
  passed: finalMergedResults.filter((r) => r.status === "passed").length,
551
608
  failed: finalMergedResults.filter((r) => r.status === "failed").length,
552
609
  skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
610
+ flaky: finalMergedResults.filter((r) => r.status === "flaky").length,
553
611
  duration: totalDuration,
554
612
  };
555
613
  const finalReport = {
556
614
  run: combinedRun,
557
- results: finalMergedResults, // Use the de-duplicated list
615
+ results: finalMergedResults,
558
616
  metadata: {
559
617
  generatedAt: new Date().toISOString(),
560
618
  },
@@ -1,9 +1,10 @@
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";
3
4
  export interface TestStep {
4
5
  id: string;
5
6
  title: string;
6
- status: TestStatus;
7
+ status: PulseTestStatus;
7
8
  duration: number;
8
9
  startTime: Date;
9
10
  endTime: Date;
@@ -18,11 +19,12 @@ export interface TestStep {
18
19
  export interface TestResult {
19
20
  id: string;
20
21
  name: string;
21
- status: TestStatus;
22
+ status: PulseTestStatus;
22
23
  duration: number;
23
24
  startTime: Date;
24
25
  endTime: Date;
25
26
  retries: number;
27
+ runCounter: number;
26
28
  steps: TestStep[];
27
29
  errorMessage?: string;
28
30
  stackTrace?: string;
@@ -54,6 +56,7 @@ export interface TestRun {
54
56
  passed: number;
55
57
  failed: number;
56
58
  skipped: number;
59
+ flaky: number;
57
60
  duration: number;
58
61
  environment?: EnvDetails;
59
62
  }
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
- "author": "Arghajit Singha",
4
- "version": "0.1.1",
3
+ "version": "0.1.2-beta-2",
5
4
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
5
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
6
  "keywords": [
@@ -51,7 +50,8 @@
51
50
  "report:merge": "node ./scripts/merge-pulse-report.js",
52
51
  "report:email": "node ./scripts/sendReport.mjs",
53
52
  "report:minify": "node ./scripts/generate-email-report.mjs",
54
- "generate-trend": "node ./scripts/generate-trend.mjs"
53
+ "generate-trend": "node ./scripts/generate-trend.mjs",
54
+ "deploy": "npm publish --access -public"
55
55
  },
56
56
  "dependencies": {
57
57
  "archiver": "^7.0.1",
@@ -1850,43 +1850,53 @@ 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
- (() => {
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>
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>
1872
1875
  <div class="attachment-caption">
1873
- <span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
1874
- <span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
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>
1875
1882
  </div>
1876
1883
  <div class="attachment-info">
1877
1884
  <div class="trace-actions">
1878
1885
  <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
1879
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
1886
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1887
+ attachment.name
1888
+ )}">Download</a>
1880
1889
  </div>
1881
1890
  </div>
1882
1891
  </div>`;
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
- }${
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
+ })()}${
1890
1900
  hasNestedSteps
1891
1901
  ? `<div class="nested-steps">${generateStepsHTML(
1892
1902
  step.steps,
@@ -1903,7 +1913,9 @@ function generateHTML(reportData, trendData = null) {
1903
1913
  test.tags || []
1904
1914
  )
1905
1915
  .join(",")
1906
- .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1916
+ .toLowerCase()}" data-test-id="${sanitizeHTML(
1917
+ String(test.id || testIndex)
1918
+ )}">
1907
1919
  <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1908
1920
  test.status
1909
1921
  )}">${String(
@@ -1970,7 +1982,9 @@ function generateHTML(reportData, trendData = null) {
1970
1982
  ${
1971
1983
  test.stderr && test.stderr.length > 0
1972
1984
  ? (() => {
1973
- const logId = `stderr-log-${test.id || testIndex}`;
1985
+ const logId = `stderr-log-${
1986
+ test.id || testIndex
1987
+ }`;
1974
1988
  return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1975
1989
  .map((line) => sanitizeHTML(line))
1976
1990
  .join("\\n")}</pre></div>`;
@@ -1984,7 +1998,7 @@ function generateHTML(reportData, trendData = null) {
1984
1998
  test.screenshots.length === 0
1985
1999
  )
1986
2000
  return "";
1987
- return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
2001
+ return `<div class="attachments-section"><h4>Screenshots (Click to load Images)</h4><div class="attachments-grid">${test.screenshots
1988
2002
  .map((screenshotPath, index) => {
1989
2003
  try {
1990
2004
  const imagePath = path.resolve(
@@ -2384,7 +2398,7 @@ aspect-ratio: 16 / 9;
2384
2398
  @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; } }
2385
2399
  @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; } }
2386
2400
  @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; } }
2387
- .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
2401
+ .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2388
2402
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
2389
2403
  .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2390
2404
  .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }