@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:
|
|
243
|
-
runId:
|
|
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 =
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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,
|
|
615
|
+
results: finalMergedResults,
|
|
558
616
|
metadata: {
|
|
559
617
|
generatedAt: new Date().toISOString(),
|
|
560
618
|
},
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
"
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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(
|
|
1874
|
-
|
|
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(
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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(
|
|
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-${
|
|
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;
|
|
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; }
|