@arghajit/dummy 0.1.1 → 0.1.2-beta-1

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,7 +20,9 @@ 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;
22
24
  private _getFinalizedResults;
25
+ private _getStatusOrder;
23
26
  onError(error: any): void;
24
27
  private _getEnvDetails;
25
28
  private _writeShardResults;
@@ -39,13 +39,23 @@ 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 a test passes on a retry, it's considered flaky regardless of expected status.
44
+ // This is the most critical check for flaky tests.
45
+ if (status === "passed" && retryCount > 0) {
46
+ return "flaky";
47
+ }
48
+ // Handle expected statuses for the final result.
43
49
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
50
+ // If expected to fail but passed, it's flaky.
51
+ if (status === "passed")
52
+ return "flaky";
44
53
  return "failed";
45
54
  }
46
55
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
47
56
  return "skipped";
48
57
  }
58
+ // Default Playwright status mapping
49
59
  switch (status) {
50
60
  case "passed":
51
61
  return "passed";
@@ -64,10 +74,12 @@ const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
64
74
  class PlaywrightPulseReporter {
65
75
  constructor(options = {}) {
66
76
  var _a, _b, _c;
77
+ // This will now store all individual run attempts for all tests.
67
78
  this.results = [];
68
79
  this.baseOutputFile = "playwright-pulse-report.json";
69
80
  this.isSharded = false;
70
81
  this.shardIndex = undefined;
82
+ this.currentRunId = ""; // Added to store the overall run ID
71
83
  this.options = options;
72
84
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
85
  this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
@@ -82,6 +94,8 @@ class PlaywrightPulseReporter {
82
94
  this.config = config;
83
95
  this.suite = suite;
84
96
  this.runStartTime = Date.now();
97
+ // Generate the overall runId once at the beginning
98
+ this.currentRunId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
85
99
  const configDir = this.config.rootDir;
86
100
  const configFileDir = this.config.configFile
87
101
  ? path.dirname(this.config.configFile)
@@ -107,7 +121,7 @@ class PlaywrightPulseReporter {
107
121
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
108
122
  }
109
123
  onTestBegin(test) {
110
- console.log(`Starting test: ${test.title}`);
124
+ // console.log(`Starting test: ${test.title}`); // Removed for brevity in final output
111
125
  }
112
126
  getBrowserDetails(test) {
113
127
  var _a, _b, _c, _d;
@@ -159,7 +173,8 @@ class PlaywrightPulseReporter {
159
173
  }
160
174
  return finalString.trim();
161
175
  }
162
- async processStep(step, testId, browserDetails, testCase) {
176
+ async processStep(step, testId, browserDetails, testCase, retryCount = 0 // Pass retryCount to convertStatus for steps
177
+ ) {
163
178
  var _a, _b, _c, _d;
164
179
  let stepStatus = "passed";
165
180
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
@@ -167,7 +182,8 @@ class PlaywrightPulseReporter {
167
182
  stepStatus = "skipped";
168
183
  }
169
184
  else {
170
- stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
185
+ // Use the extended convertStatus
186
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase, retryCount);
171
187
  }
172
188
  const duration = step.duration;
173
189
  const startTime = new Date(step.startTime);
@@ -200,13 +216,15 @@ class PlaywrightPulseReporter {
200
216
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
201
217
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
218
  const browserDetails = this.getBrowserDetails(test);
203
- const testStatus = convertStatus(result.status, test);
219
+ // Use the extended convertStatus, passing result.retry
220
+ const testStatus = convertStatus(result.status, test, result.retry);
204
221
  const startTime = new Date(result.startTime);
205
222
  const endTime = new Date(startTime.getTime() + result.duration);
206
223
  const processAllSteps = async (steps) => {
207
224
  let processed = [];
208
225
  for (const step of steps) {
209
- const processedStep = await this.processStep(step, test.id, browserDetails, test);
226
+ const processedStep = await this.processStep(step, test.id, browserDetails, test, result.retry // Pass retryCount to processStep
227
+ );
210
228
  processed.push(processedStep);
211
229
  if (step.steps && step.steps.length > 0) {
212
230
  processedStep.steps = await processAllSteps(step.steps);
@@ -238,9 +256,11 @@ class PlaywrightPulseReporter {
238
256
  ? JSON.stringify(this.config.metadata)
239
257
  : undefined,
240
258
  };
259
+ // Correctly handle the ID for each run. A unique ID per attempt is crucial.
260
+ const testIdWithRunCounter = `${test.id}-run-${result.retry}`;
241
261
  const pulseResult = {
242
- id: test.id,
243
- runId: "TBD",
262
+ id: testIdWithRunCounter, // Use the modified ID
263
+ runId: this.currentRunId, // Assign the overall run ID
244
264
  name: test.titlePath().join(" > "),
245
265
  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
266
  status: testStatus,
@@ -248,7 +268,8 @@ class PlaywrightPulseReporter {
248
268
  startTime: startTime,
249
269
  endTime: endTime,
250
270
  browser: browserDetails,
251
- retries: result.retry,
271
+ retries: result.retry, // This is the Playwright retry count (0 for first run, 1 for first retry, etc.)
272
+ runCounter: result.retry, // This is your 'runCounter'
252
273
  steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
253
274
  errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
254
275
  stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
@@ -267,7 +288,8 @@ class PlaywrightPulseReporter {
267
288
  if (!attachment.path)
268
289
  continue;
269
290
  try {
270
- const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
291
+ // Use the new testIdWithRunCounter for the subfolder
292
+ const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
271
293
  const safeAttachmentName = path
272
294
  .basename(attachment.path)
273
295
  .replace(/[^a-zA-Z0-9_.-]/g, "_");
@@ -299,17 +321,68 @@ class PlaywrightPulseReporter {
299
321
  }
300
322
  this.results.push(pulseResult);
301
323
  }
324
+ // New method to extract the base test ID, ignoring the run-counter suffix
325
+ _getBaseTestId(testResultId) {
326
+ const parts = testResultId.split("-run-");
327
+ return parts[0];
328
+ }
302
329
  _getFinalizedResults(allResults) {
303
330
  const finalResultsMap = new Map();
331
+ const allRunsMap = new Map();
332
+ // First, group all run attempts by their base test ID
304
333
  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);
334
+ const baseTestId = this._getBaseTestId(result.id);
335
+ if (!allRunsMap.has(baseTestId)) {
336
+ allRunsMap.set(baseTestId, []);
337
+ }
338
+ allRunsMap.get(baseTestId).push(result);
339
+ }
340
+ // Now, iterate through the grouped runs to determine the final state
341
+ for (const [baseTestId, runs] of allRunsMap.entries()) {
342
+ let finalResult = undefined;
343
+ // Sort runs to process them in chronological order
344
+ runs.sort((a, b) => a.runCounter - b.runCounter);
345
+ for (const currentRun of runs) {
346
+ if (!finalResult) {
347
+ finalResult = currentRun;
348
+ }
349
+ else {
350
+ // Compare the current run to the best result found so far
351
+ const currentStatusOrder = this._getStatusOrder(currentRun.status);
352
+ const finalStatusOrder = this._getStatusOrder(finalResult.status);
353
+ if (currentStatusOrder < finalStatusOrder) {
354
+ // Current run is "better" (e.g., passed over failed)
355
+ finalResult = currentRun;
356
+ }
357
+ else if (currentStatusOrder === finalStatusOrder &&
358
+ currentRun.retries > finalResult.retries) {
359
+ // Same status, but prefer the latest attempt
360
+ finalResult = currentRun;
361
+ }
362
+ }
363
+ }
364
+ if (finalResult) {
365
+ // Ensure the ID of the final result is the base test ID for de-duplication
366
+ finalResult.id = baseTestId;
367
+ finalResultsMap.set(baseTestId, finalResult);
309
368
  }
310
369
  }
311
370
  return Array.from(finalResultsMap.values());
312
371
  }
372
+ _getStatusOrder(status) {
373
+ switch (status) {
374
+ case "passed":
375
+ return 1;
376
+ case "flaky":
377
+ return 2;
378
+ case "failed":
379
+ return 3;
380
+ case "skipped":
381
+ return 4;
382
+ default:
383
+ return 99; // Unknown status
384
+ }
385
+ }
313
386
  onError(error) {
314
387
  var _a;
315
388
  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);
@@ -344,15 +417,14 @@ class PlaywrightPulseReporter {
344
417
  }
345
418
  }
346
419
  async _mergeShardResults(finalRunData) {
347
- let allShardProcessedResults = [];
420
+ let allShardRawResults = []; // Store raw results before final de-duplication
348
421
  const totalShards = this.config.shard ? this.config.shard.total : 1;
349
422
  for (let i = 0; i < totalShards; i++) {
350
423
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
351
424
  try {
352
425
  const content = await fs.readFile(tempFilePath, "utf-8");
353
426
  const shardResults = JSON.parse(content);
354
- allShardProcessedResults =
355
- allShardProcessedResults.concat(shardResults);
427
+ allShardRawResults = allShardRawResults.concat(shardResults);
356
428
  }
357
429
  catch (error) {
358
430
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
@@ -363,11 +435,14 @@ class PlaywrightPulseReporter {
363
435
  }
364
436
  }
365
437
  }
366
- const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
438
+ // Apply _getFinalizedResults after all raw shard results are collected
439
+ const finalResultsList = this._getFinalizedResults(allShardRawResults);
367
440
  finalResultsList.forEach((r) => (r.runId = finalRunData.id));
368
441
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
369
442
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
370
443
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
444
+ // Add flaky count
445
+ finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
371
446
  finalRunData.totalTests = finalResultsList.length;
372
447
  const reviveDates = (key, value) => {
373
448
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -414,34 +489,36 @@ class PlaywrightPulseReporter {
414
489
  await this._writeShardResults();
415
490
  return;
416
491
  }
417
- // De-duplicate and handle retries here, in a safe, single-threaded context.
492
+ // Now, `this.results` contains all individual run attempts.
493
+ // _getFinalizedResults will select the "best" run for each logical test.
418
494
  const finalResults = this._getFinalizedResults(this.results);
419
495
  const runEndTime = Date.now();
420
496
  const duration = runEndTime - this.runStartTime;
421
- const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
497
+ // Use the stored overall runId
498
+ const runId = this.currentRunId;
422
499
  const environmentDetails = this._getEnvDetails();
423
500
  const runData = {
424
501
  id: runId,
425
502
  timestamp: new Date(this.runStartTime),
426
- // Use the length of the de-duplicated array for all counts
427
503
  totalTests: finalResults.length,
428
504
  passed: finalResults.filter((r) => r.status === "passed").length,
429
505
  failed: finalResults.filter((r) => r.status === "failed").length,
430
506
  skipped: finalResults.filter((r) => r.status === "skipped").length,
507
+ flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
431
508
  duration,
432
509
  environment: environmentDetails,
433
510
  };
511
+ // Ensure all final results have the correct overall runId
434
512
  finalResults.forEach((r) => (r.runId = runId));
435
513
  let finalReport = undefined;
436
514
  if (this.isSharded) {
437
- // The _mergeShardResults method will handle its own de-duplication
515
+ // _mergeShardResults will now perform the final de-duplication across shards
438
516
  finalReport = await this._mergeShardResults(runData);
439
517
  }
440
518
  else {
441
519
  finalReport = {
442
520
  run: runData,
443
- // Use the de-duplicated results
444
- results: finalResults,
521
+ results: finalResults, // Use the de-duplicated results for a non-sharded run
445
522
  metadata: { generatedAt: new Date().toISOString() },
446
523
  };
447
524
  }
@@ -470,7 +547,6 @@ class PlaywrightPulseReporter {
470
547
  }
471
548
  }
472
549
  else {
473
- // Logic for appending/merging reports
474
550
  const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
475
551
  const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
476
552
  try {
@@ -516,7 +592,10 @@ class PlaywrightPulseReporter {
516
592
  const allResultsFromAllFiles = [];
517
593
  let latestTimestamp = new Date(0);
518
594
  let lastRunEnvironment = undefined;
519
- let totalDuration = 0;
595
+ // We can't simply sum durations across merged files, as the tests might overlap.
596
+ // The final duration will be derived from the range of start/end times in the final results.
597
+ let earliestStartTime = Date.now();
598
+ let latestEndTime = 0;
520
599
  for (const file of reportFiles) {
521
600
  const filePath = path.join(pulseResultsDir, file);
522
601
  try {
@@ -539,22 +618,28 @@ class PlaywrightPulseReporter {
539
618
  }
540
619
  // De-duplicate the results from ALL merged files using the helper function
541
620
  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);
621
+ // Calculate overall duration from the earliest start and latest end of the final merged results
622
+ for (const res of finalMergedResults) {
623
+ if (res.startTime.getTime() < earliestStartTime)
624
+ earliestStartTime = res.startTime.getTime();
625
+ if (res.endTime.getTime() > latestEndTime)
626
+ latestEndTime = res.endTime.getTime();
627
+ }
628
+ const totalDuration = latestEndTime > earliestStartTime ? latestEndTime - earliestStartTime : 0;
544
629
  const combinedRun = {
545
630
  id: `merged-${Date.now()}`,
546
631
  timestamp: latestTimestamp,
547
632
  environment: lastRunEnvironment,
548
- // Recalculate counts based on the truly final, de-duplicated list
549
633
  totalTests: finalMergedResults.length,
550
634
  passed: finalMergedResults.filter((r) => r.status === "passed").length,
551
635
  failed: finalMergedResults.filter((r) => r.status === "failed").length,
552
636
  skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
637
+ flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
553
638
  duration: totalDuration,
554
639
  };
555
640
  const finalReport = {
556
641
  run: combinedRun,
557
- results: finalMergedResults, // Use the de-duplicated list
642
+ results: finalMergedResults,
558
643
  metadata: {
559
644
  generatedAt: new Date().toISOString(),
560
645
  },
@@ -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-1",
5
4
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
5
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
6
  "keywords": [
@@ -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; }