@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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
259
|
-
runId:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
599
|
-
|
|
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
|
},
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
"
|
|
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.
|
|
76
|
+
"eslint": "^9.39.1",
|
|
76
77
|
"typescript": "^5"
|
|
77
78
|
},
|
|
78
79
|
"engines": {
|
|
79
|
-
"node": ">=
|
|
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
|
-
|
|
1855
|
-
return "";
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
|
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; }
|