@arghajit/dummy 0.1.2 → 0.3.0
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 +8 -4
- package/dist/reporter/playwright-pulse-reporter.d.ts +0 -2
- package/dist/reporter/playwright-pulse-reporter.js +35 -96
- package/dist/types/index.d.ts +11 -5
- package/package.json +9 -4
- package/scripts/generate-report.mjs +228 -148
- package/scripts/generate-static-report.mjs +107 -11
- package/scripts/sendReport.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Playwright Pluse Report
|
|
2
2
|
|
|
3
|
-
](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
4
9
|
_The ultimate Playwright reporter — Interactive dashboard with historical trend analytics, CI/CD-ready standalone HTML reports, and sharding support for scalable test execution._
|
|
5
10
|
|
|
6
11
|
## [Live Demo](https://pulse-report.netlify.app/)
|
|
@@ -16,7 +21,7 @@ The project provides these utility commands:
|
|
|
16
21
|
| Command | Description |
|
|
17
22
|
|------------------------|-----------------------------------------------------------------------------|
|
|
18
23
|
| `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
|
|
24
|
+
| `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
25
|
| `merge-pulse-report` | Combines multiple parallel test json reports, basically used in sharding |
|
|
21
26
|
| `generate-trend` | Analyzes historical trends in test results |
|
|
22
27
|
| `generate-email-report`| Generates email-friendly report versions |
|
|
@@ -272,8 +277,7 @@ export default defineConfig({
|
|
|
272
277
|
|
|
273
278
|
---
|
|
274
279
|
|
|
275
|
-
|
|
276
|
-
<h2>Pulse Dashboard</h2>
|
|
280
|
+

|
|
277
281
|
|
|
278
282
|
**Real-time Playwright Test Monitoring & Analysis**
|
|
279
283
|
|
|
@@ -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);
|
|
@@ -209,18 +197,16 @@ class PlaywrightPulseReporter {
|
|
|
209
197
|
};
|
|
210
198
|
}
|
|
211
199
|
async onTestEnd(test, result) {
|
|
212
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
200
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
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,
|
|
@@ -278,14 +261,14 @@ class PlaywrightPulseReporter {
|
|
|
278
261
|
attachments: [],
|
|
279
262
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
280
263
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
264
|
+
annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
|
|
281
265
|
...testSpecificData,
|
|
282
266
|
};
|
|
283
267
|
for (const [index, attachment] of result.attachments.entries()) {
|
|
284
268
|
if (!attachment.path)
|
|
285
269
|
continue;
|
|
286
270
|
try {
|
|
287
|
-
|
|
288
|
-
const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
271
|
+
const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
289
272
|
const safeAttachmentName = path
|
|
290
273
|
.basename(attachment.path)
|
|
291
274
|
.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
@@ -295,16 +278,16 @@ class PlaywrightPulseReporter {
|
|
|
295
278
|
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
296
279
|
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
297
280
|
if (attachment.contentType.startsWith("image/")) {
|
|
298
|
-
(
|
|
281
|
+
(_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
|
|
299
282
|
}
|
|
300
283
|
else if (attachment.contentType.startsWith("video/")) {
|
|
301
|
-
(
|
|
284
|
+
(_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
|
|
302
285
|
}
|
|
303
286
|
else if (attachment.name === "trace") {
|
|
304
287
|
pulseResult.tracePath = relativeDestPath;
|
|
305
288
|
}
|
|
306
289
|
else {
|
|
307
|
-
(
|
|
290
|
+
(_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
|
|
308
291
|
name: attachment.name,
|
|
309
292
|
path: relativeDestPath,
|
|
310
293
|
contentType: attachment.contentType,
|
|
@@ -320,46 +303,14 @@ class PlaywrightPulseReporter {
|
|
|
320
303
|
_getFinalizedResults(allResults) {
|
|
321
304
|
const finalResultsMap = new Map();
|
|
322
305
|
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
|
-
}
|
|
306
|
+
const existing = finalResultsMap.get(result.id);
|
|
307
|
+
// Keep the result with the highest retry attempt for each test ID
|
|
308
|
+
if (!existing || result.retries >= existing.retries) {
|
|
309
|
+
finalResultsMap.set(result.id, result);
|
|
345
310
|
}
|
|
346
311
|
}
|
|
347
312
|
return Array.from(finalResultsMap.values());
|
|
348
313
|
}
|
|
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
314
|
onError(error) {
|
|
364
315
|
var _a;
|
|
365
316
|
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 +345,15 @@ class PlaywrightPulseReporter {
|
|
|
394
345
|
}
|
|
395
346
|
}
|
|
396
347
|
async _mergeShardResults(finalRunData) {
|
|
397
|
-
let
|
|
348
|
+
let allShardProcessedResults = [];
|
|
398
349
|
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
399
350
|
for (let i = 0; i < totalShards; i++) {
|
|
400
351
|
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
|
|
401
352
|
try {
|
|
402
353
|
const content = await fs.readFile(tempFilePath, "utf-8");
|
|
403
354
|
const shardResults = JSON.parse(content);
|
|
404
|
-
|
|
355
|
+
allShardProcessedResults =
|
|
356
|
+
allShardProcessedResults.concat(shardResults);
|
|
405
357
|
}
|
|
406
358
|
catch (error) {
|
|
407
359
|
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
|
|
@@ -412,14 +364,11 @@ class PlaywrightPulseReporter {
|
|
|
412
364
|
}
|
|
413
365
|
}
|
|
414
366
|
}
|
|
415
|
-
|
|
416
|
-
const finalResultsList = this._getFinalizedResults(allShardRawResults);
|
|
367
|
+
const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
|
|
417
368
|
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
|
|
418
369
|
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
419
370
|
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
420
371
|
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
|
|
421
|
-
// Add flaky count
|
|
422
|
-
finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
|
|
423
372
|
finalRunData.totalTests = finalResultsList.length;
|
|
424
373
|
const reviveDates = (key, value) => {
|
|
425
374
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
@@ -466,36 +415,34 @@ class PlaywrightPulseReporter {
|
|
|
466
415
|
await this._writeShardResults();
|
|
467
416
|
return;
|
|
468
417
|
}
|
|
469
|
-
//
|
|
470
|
-
// _getFinalizedResults will select the "best" run for each logical test.
|
|
418
|
+
// De-duplicate and handle retries here, in a safe, single-threaded context.
|
|
471
419
|
const finalResults = this._getFinalizedResults(this.results);
|
|
472
420
|
const runEndTime = Date.now();
|
|
473
421
|
const duration = runEndTime - this.runStartTime;
|
|
474
|
-
|
|
475
|
-
const runId = this.currentRunId;
|
|
422
|
+
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
|
|
476
423
|
const environmentDetails = this._getEnvDetails();
|
|
477
424
|
const runData = {
|
|
478
425
|
id: runId,
|
|
479
426
|
timestamp: new Date(this.runStartTime),
|
|
427
|
+
// Use the length of the de-duplicated array for all counts
|
|
480
428
|
totalTests: finalResults.length,
|
|
481
429
|
passed: finalResults.filter((r) => r.status === "passed").length,
|
|
482
430
|
failed: finalResults.filter((r) => r.status === "failed").length,
|
|
483
431
|
skipped: finalResults.filter((r) => r.status === "skipped").length,
|
|
484
|
-
flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
|
|
485
432
|
duration,
|
|
486
433
|
environment: environmentDetails,
|
|
487
434
|
};
|
|
488
|
-
// Ensure all final results have the correct overall runId
|
|
489
435
|
finalResults.forEach((r) => (r.runId = runId));
|
|
490
436
|
let finalReport = undefined;
|
|
491
437
|
if (this.isSharded) {
|
|
492
|
-
// _mergeShardResults will
|
|
438
|
+
// The _mergeShardResults method will handle its own de-duplication
|
|
493
439
|
finalReport = await this._mergeShardResults(runData);
|
|
494
440
|
}
|
|
495
441
|
else {
|
|
496
442
|
finalReport = {
|
|
497
443
|
run: runData,
|
|
498
|
-
|
|
444
|
+
// Use the de-duplicated results
|
|
445
|
+
results: finalResults,
|
|
499
446
|
metadata: { generatedAt: new Date().toISOString() },
|
|
500
447
|
};
|
|
501
448
|
}
|
|
@@ -524,6 +471,7 @@ class PlaywrightPulseReporter {
|
|
|
524
471
|
}
|
|
525
472
|
}
|
|
526
473
|
else {
|
|
474
|
+
// Logic for appending/merging reports
|
|
527
475
|
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
528
476
|
const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
|
|
529
477
|
try {
|
|
@@ -569,10 +517,7 @@ class PlaywrightPulseReporter {
|
|
|
569
517
|
const allResultsFromAllFiles = [];
|
|
570
518
|
let latestTimestamp = new Date(0);
|
|
571
519
|
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;
|
|
520
|
+
let totalDuration = 0;
|
|
576
521
|
for (const file of reportFiles) {
|
|
577
522
|
const filePath = path.join(pulseResultsDir, file);
|
|
578
523
|
try {
|
|
@@ -595,28 +540,22 @@ class PlaywrightPulseReporter {
|
|
|
595
540
|
}
|
|
596
541
|
// De-duplicate the results from ALL merged files using the helper function
|
|
597
542
|
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;
|
|
543
|
+
// Sum the duration from the final, de-duplicated list of tests
|
|
544
|
+
totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
|
|
606
545
|
const combinedRun = {
|
|
607
546
|
id: `merged-${Date.now()}`,
|
|
608
547
|
timestamp: latestTimestamp,
|
|
609
548
|
environment: lastRunEnvironment,
|
|
549
|
+
// Recalculate counts based on the truly final, de-duplicated list
|
|
610
550
|
totalTests: finalMergedResults.length,
|
|
611
551
|
passed: finalMergedResults.filter((r) => r.status === "passed").length,
|
|
612
552
|
failed: finalMergedResults.filter((r) => r.status === "failed").length,
|
|
613
553
|
skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
|
|
614
|
-
flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
|
|
615
554
|
duration: totalDuration,
|
|
616
555
|
};
|
|
617
556
|
const finalReport = {
|
|
618
557
|
run: combinedRun,
|
|
619
|
-
results: finalMergedResults,
|
|
558
|
+
results: finalMergedResults, // Use the de-duplicated list
|
|
620
559
|
metadata: {
|
|
621
560
|
generatedAt: new Date().toISOString(),
|
|
622
561
|
},
|
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;
|
|
@@ -48,6 +46,15 @@ export interface TestResult {
|
|
|
48
46
|
totalWorkers?: number;
|
|
49
47
|
configFile?: string;
|
|
50
48
|
metadata?: string;
|
|
49
|
+
annotations?: {
|
|
50
|
+
type: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
location?: {
|
|
53
|
+
file: string;
|
|
54
|
+
line: number;
|
|
55
|
+
column: number;
|
|
56
|
+
};
|
|
57
|
+
}[];
|
|
51
58
|
}
|
|
52
59
|
export interface TestRun {
|
|
53
60
|
id: string;
|
|
@@ -56,7 +63,6 @@ export interface TestRun {
|
|
|
56
63
|
passed: number;
|
|
57
64
|
failed: number;
|
|
58
65
|
skipped: number;
|
|
59
|
-
flaky: number;
|
|
60
66
|
duration: number;
|
|
61
67
|
environment?: EnvDetails;
|
|
62
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
|
-
"
|
|
3
|
+
"author": "Arghajit Singha",
|
|
4
|
+
"version": "0.3.0",
|
|
4
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
5
6
|
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
6
7
|
"keywords": [
|
|
@@ -17,7 +18,8 @@
|
|
|
17
18
|
"send-report",
|
|
18
19
|
"email",
|
|
19
20
|
"playwright-report",
|
|
20
|
-
"pulse"
|
|
21
|
+
"pulse",
|
|
22
|
+
"ai-failure-analysis"
|
|
21
23
|
],
|
|
22
24
|
"main": "dist/reporter/index.js",
|
|
23
25
|
"types": "dist/reporter/index.d.ts",
|
|
@@ -72,13 +74,16 @@
|
|
|
72
74
|
"devDependencies": {
|
|
73
75
|
"@types/node": "^20",
|
|
74
76
|
"@types/ua-parser-js": "^0.7.39",
|
|
75
|
-
"eslint": "9.
|
|
77
|
+
"eslint": "^9.39.1",
|
|
76
78
|
"typescript": "^5"
|
|
77
79
|
},
|
|
78
80
|
"engines": {
|
|
79
|
-
"node": ">=
|
|
81
|
+
"node": ">=18"
|
|
80
82
|
},
|
|
81
83
|
"peerDependencies": {
|
|
82
84
|
"@playwright/test": ">=1.40.0"
|
|
85
|
+
},
|
|
86
|
+
"overrides": {
|
|
87
|
+
"glob": "^13.0.0"
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -1713,41 +1713,86 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1713
1713
|
};
|
|
1714
1714
|
|
|
1715
1715
|
return `
|
|
1716
|
-
<div class="test-case" data-status="${
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1716
|
+
<div class="test-case" data-status="${
|
|
1717
|
+
test.status
|
|
1718
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1719
|
+
.join(",")
|
|
1720
|
+
.toLowerCase()}">
|
|
1720
1721
|
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1721
1722
|
<div class="test-case-summary">
|
|
1722
1723
|
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
1723
|
-
|
|
1724
|
-
|
|
1724
|
+
test.status
|
|
1725
|
+
).toUpperCase()}</span>
|
|
1725
1726
|
<span class="test-case-title" title="${sanitizeHTML(
|
|
1726
1727
|
test.name
|
|
1727
1728
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
1728
1729
|
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
1729
1730
|
</div>
|
|
1730
1731
|
<div class="test-case-meta">
|
|
1731
|
-
${
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1732
|
+
${
|
|
1733
|
+
test.tags && test.tags.length > 0
|
|
1734
|
+
? test.tags
|
|
1735
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1736
|
+
.join(" ")
|
|
1737
|
+
: ""
|
|
1738
|
+
}
|
|
1737
1739
|
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1738
1740
|
</div>
|
|
1739
1741
|
</div>
|
|
1740
1742
|
<div class="test-case-content" style="display: none;">
|
|
1741
1743
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1744
|
+
${
|
|
1745
|
+
test.annotations && test.annotations.length > 0
|
|
1746
|
+
? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
|
|
1747
|
+
<h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
|
|
1748
|
+
${test.annotations
|
|
1749
|
+
.map((annotation, idx) => {
|
|
1750
|
+
const isIssueOrBug =
|
|
1751
|
+
annotation.type === "issue" ||
|
|
1752
|
+
annotation.type === "bug";
|
|
1753
|
+
const descriptionText = annotation.description || "";
|
|
1754
|
+
const typeLabel = sanitizeHTML(annotation.type);
|
|
1755
|
+
const descriptionHtml =
|
|
1756
|
+
isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
|
|
1757
|
+
? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
|
|
1758
|
+
descriptionText
|
|
1759
|
+
)}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
|
|
1760
|
+
descriptionText
|
|
1761
|
+
)}</a>`
|
|
1762
|
+
: sanitizeHTML(descriptionText);
|
|
1763
|
+
const locationText = annotation.location
|
|
1764
|
+
? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
|
|
1765
|
+
annotation.location.file
|
|
1766
|
+
)}:${annotation.location.line}:${
|
|
1767
|
+
annotation.location.column
|
|
1768
|
+
}</div>`
|
|
1769
|
+
: "";
|
|
1770
|
+
return `<div style="margin-bottom: ${
|
|
1771
|
+
idx < test.annotations.length - 1 ? "10px" : "0"
|
|
1772
|
+
};">
|
|
1773
|
+
<strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
|
|
1774
|
+
${
|
|
1775
|
+
descriptionText
|
|
1776
|
+
? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
|
|
1777
|
+
: ""
|
|
1778
|
+
}
|
|
1779
|
+
${locationText}
|
|
1780
|
+
</div>`;
|
|
1781
|
+
})
|
|
1782
|
+
.join("")}
|
|
1783
|
+
</div>`
|
|
1784
|
+
: ""
|
|
1785
|
+
}
|
|
1742
1786
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1743
1787
|
test.workerId
|
|
1744
1788
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
${
|
|
1748
|
-
|
|
1749
|
-
test
|
|
1750
|
-
|
|
1789
|
+
test.totalWorkers
|
|
1790
|
+
)}]</p>
|
|
1791
|
+
${
|
|
1792
|
+
test.errorMessage
|
|
1793
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1794
|
+
test.errorMessage
|
|
1795
|
+
)}
|
|
1751
1796
|
<button
|
|
1752
1797
|
class="copy-error-btn"
|
|
1753
1798
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1768,13 +1813,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1768
1813
|
Copy Error Prompt
|
|
1769
1814
|
</button>
|
|
1770
1815
|
</div>`
|
|
1771
|
-
|
|
1816
|
+
: ""
|
|
1772
1817
|
}
|
|
1773
|
-
${
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1818
|
+
${
|
|
1819
|
+
test.snippet
|
|
1820
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1821
|
+
test.snippet
|
|
1822
|
+
)}</code></pre></div>`
|
|
1823
|
+
: ""
|
|
1778
1824
|
}
|
|
1779
1825
|
<h4>Steps</h4>
|
|
1780
1826
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
@@ -1793,75 +1839,86 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1793
1839
|
</div>
|
|
1794
1840
|
</div>`;
|
|
1795
1841
|
})()}
|
|
1796
|
-
${
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1842
|
+
${
|
|
1843
|
+
test.stderr && test.stderr.length > 0
|
|
1844
|
+
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
1845
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
1846
|
+
)}</pre></div>`
|
|
1847
|
+
: ""
|
|
1801
1848
|
}
|
|
1802
|
-
${
|
|
1803
|
-
|
|
1849
|
+
${
|
|
1850
|
+
test.screenshots && test.screenshots.length > 0
|
|
1851
|
+
? `
|
|
1804
1852
|
<div class="attachments-section">
|
|
1805
1853
|
<h4>Screenshots</h4>
|
|
1806
1854
|
<div class="attachments-grid">
|
|
1807
1855
|
${test.screenshots
|
|
1808
|
-
|
|
1809
|
-
|
|
1856
|
+
.map(
|
|
1857
|
+
(screenshot, index) => `
|
|
1810
1858
|
<div class="attachment-item">
|
|
1811
|
-
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
1859
|
+
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
1860
|
+
index + 1
|
|
1861
|
+
}">
|
|
1812
1862
|
<div class="attachment-info">
|
|
1813
1863
|
<div class="trace-actions">
|
|
1814
|
-
<a href="${fixPath(
|
|
1815
|
-
|
|
1864
|
+
<a href="${fixPath(
|
|
1865
|
+
screenshot
|
|
1866
|
+
)}" target="_blank" class="view-full">View Full Image</a>
|
|
1867
|
+
<a href="${fixPath(
|
|
1868
|
+
screenshot
|
|
1869
|
+
)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1816
1870
|
</div>
|
|
1817
1871
|
</div>
|
|
1818
1872
|
</div>
|
|
1819
1873
|
`
|
|
1820
|
-
|
|
1821
|
-
|
|
1874
|
+
)
|
|
1875
|
+
.join("")}
|
|
1822
1876
|
</div>
|
|
1823
1877
|
</div>
|
|
1824
1878
|
`
|
|
1825
|
-
|
|
1879
|
+
: ""
|
|
1826
1880
|
}
|
|
1827
|
-
${
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1881
|
+
${
|
|
1882
|
+
test.videoPath && test.videoPath.length > 0
|
|
1883
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1884
|
+
.map((videoUrl, index) => {
|
|
1885
|
+
const fixedVideoUrl = fixPath(videoUrl);
|
|
1886
|
+
const fileExtension = String(fixedVideoUrl)
|
|
1887
|
+
.split(".")
|
|
1888
|
+
.pop()
|
|
1889
|
+
.toLowerCase();
|
|
1890
|
+
const mimeType =
|
|
1891
|
+
{
|
|
1892
|
+
mp4: "video/mp4",
|
|
1893
|
+
webm: "video/webm",
|
|
1894
|
+
ogg: "video/ogg",
|
|
1895
|
+
mov: "video/quicktime",
|
|
1896
|
+
avi: "video/x-msvideo",
|
|
1897
|
+
}[fileExtension] || "video/mp4";
|
|
1898
|
+
return `<div class="attachment-item video-item">
|
|
1899
|
+
<video controls width="100%" height="auto" title="Video ${
|
|
1900
|
+
index + 1
|
|
1901
|
+
}">
|
|
1846
1902
|
<source src="${sanitizeHTML(
|
|
1847
|
-
|
|
1848
|
-
|
|
1903
|
+
fixedVideoUrl
|
|
1904
|
+
)}" type="${mimeType}">
|
|
1849
1905
|
Your browser does not support the video tag.
|
|
1850
1906
|
</video>
|
|
1851
1907
|
<div class="attachment-info">
|
|
1852
1908
|
<div class="trace-actions">
|
|
1853
1909
|
<a href="${sanitizeHTML(
|
|
1854
|
-
|
|
1855
|
-
|
|
1910
|
+
fixedVideoUrl
|
|
1911
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1856
1912
|
</div>
|
|
1857
1913
|
</div>
|
|
1858
1914
|
</div>`;
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1915
|
+
})
|
|
1916
|
+
.join("")}</div></div>`
|
|
1917
|
+
: ""
|
|
1862
1918
|
}
|
|
1863
|
-
${
|
|
1864
|
-
|
|
1919
|
+
${
|
|
1920
|
+
test.tracePath
|
|
1921
|
+
? `
|
|
1865
1922
|
<div class="attachments-section">
|
|
1866
1923
|
<h4>Trace Files</h4>
|
|
1867
1924
|
<div class="attachments-grid">
|
|
@@ -1869,70 +1926,72 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1869
1926
|
<div class="trace-preview">
|
|
1870
1927
|
<span class="trace-icon">📄</span>
|
|
1871
1928
|
<span class="trace-name">${sanitizeHTML(
|
|
1872
|
-
|
|
1873
|
-
|
|
1929
|
+
path.basename(test.tracePath)
|
|
1930
|
+
)}</span>
|
|
1874
1931
|
</div>
|
|
1875
1932
|
<div class="attachment-info">
|
|
1876
1933
|
<div class="trace-actions">
|
|
1877
1934
|
<a href="${sanitizeHTML(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1935
|
+
fixPath(test.tracePath)
|
|
1936
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1937
|
+
path.basename(test.tracePath)
|
|
1938
|
+
)}" class="download-trace">Download Trace</a>
|
|
1882
1939
|
</div>
|
|
1883
1940
|
</div>
|
|
1884
1941
|
</div>
|
|
1885
1942
|
</div>
|
|
1886
1943
|
</div>
|
|
1887
1944
|
`
|
|
1888
|
-
|
|
1945
|
+
: ""
|
|
1889
1946
|
}
|
|
1890
|
-
${
|
|
1891
|
-
|
|
1947
|
+
${
|
|
1948
|
+
test.attachments && test.attachments.length > 0
|
|
1949
|
+
? `
|
|
1892
1950
|
<div class="attachments-section">
|
|
1893
1951
|
<h4>Other Attachments</h4>
|
|
1894
1952
|
<div class="attachments-grid">
|
|
1895
1953
|
${test.attachments
|
|
1896
|
-
|
|
1897
|
-
|
|
1954
|
+
.map(
|
|
1955
|
+
(attachment) => `
|
|
1898
1956
|
<div class="attachment-item generic-attachment">
|
|
1899
1957
|
<div class="attachment-icon">${getAttachmentIcon(
|
|
1900
|
-
|
|
1901
|
-
|
|
1958
|
+
attachment.contentType
|
|
1959
|
+
)}</div>
|
|
1902
1960
|
<div class="attachment-caption">
|
|
1903
1961
|
<span class="attachment-name" title="${sanitizeHTML(
|
|
1904
|
-
|
|
1905
|
-
|
|
1962
|
+
attachment.name
|
|
1963
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1906
1964
|
<span class="attachment-type">${sanitizeHTML(
|
|
1907
|
-
|
|
1908
|
-
|
|
1965
|
+
attachment.contentType
|
|
1966
|
+
)}</span>
|
|
1909
1967
|
</div>
|
|
1910
1968
|
<div class="attachment-info">
|
|
1911
1969
|
<div class="trace-actions">
|
|
1912
1970
|
<a href="${sanitizeHTML(
|
|
1913
|
-
|
|
1914
|
-
|
|
1971
|
+
fixPath(attachment.path)
|
|
1972
|
+
)}" target="_blank" class="view-full">View</a>
|
|
1915
1973
|
<a href="${sanitizeHTML(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1974
|
+
fixPath(attachment.path)
|
|
1975
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1976
|
+
attachment.name
|
|
1977
|
+
)}" class="download-trace">Download</a>
|
|
1920
1978
|
</div>
|
|
1921
1979
|
</div>
|
|
1922
1980
|
</div>
|
|
1923
1981
|
`
|
|
1924
|
-
|
|
1925
|
-
|
|
1982
|
+
)
|
|
1983
|
+
.join("")}
|
|
1926
1984
|
</div>
|
|
1927
1985
|
</div>
|
|
1928
1986
|
`
|
|
1929
|
-
|
|
1987
|
+
: ""
|
|
1930
1988
|
}
|
|
1931
|
-
${
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1989
|
+
${
|
|
1990
|
+
test.codeSnippet
|
|
1991
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1992
|
+
sanitizeHTML(test.codeSnippet)
|
|
1993
|
+
)}</code></pre></div>`
|
|
1994
|
+
: ""
|
|
1936
1995
|
}
|
|
1937
1996
|
</div>
|
|
1938
1997
|
</div>`;
|
|
@@ -2345,8 +2404,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2345
2404
|
<h1>Playwright Pulse Report</h1>
|
|
2346
2405
|
</div>
|
|
2347
2406
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2348
|
-
|
|
2349
|
-
|
|
2407
|
+
runSummary.timestamp
|
|
2408
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2350
2409
|
runSummary.duration
|
|
2351
2410
|
)}</div>
|
|
2352
2411
|
</header>
|
|
@@ -2358,35 +2417,40 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2358
2417
|
</div>
|
|
2359
2418
|
<div id="dashboard" class="tab-content active">
|
|
2360
2419
|
<div class="dashboard-grid">
|
|
2361
|
-
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
<div class="summary-card status-
|
|
2368
|
-
|
|
2420
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2421
|
+
runSummary.totalTests
|
|
2422
|
+
}</div></div>
|
|
2423
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
2424
|
+
runSummary.passed
|
|
2425
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2426
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
2427
|
+
runSummary.failed
|
|
2428
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2429
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
2430
|
+
runSummary.skipped || 0
|
|
2431
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2369
2432
|
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2370
2433
|
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2371
|
-
|
|
2372
|
-
|
|
2434
|
+
runSummary.duration
|
|
2435
|
+
)}</div></div>
|
|
2373
2436
|
</div>
|
|
2374
2437
|
<div class="dashboard-bottom-row">
|
|
2375
2438
|
<div style="display: grid; gap: 20px">
|
|
2376
2439
|
${generatePieChart(
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
${
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2440
|
+
[
|
|
2441
|
+
{ label: "Passed", value: runSummary.passed },
|
|
2442
|
+
{ label: "Failed", value: runSummary.failed },
|
|
2443
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
2444
|
+
],
|
|
2445
|
+
400,
|
|
2446
|
+
390
|
|
2447
|
+
)}
|
|
2448
|
+
${
|
|
2449
|
+
runSummary.environment &&
|
|
2450
|
+
Object.keys(runSummary.environment).length > 0
|
|
2451
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2452
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2453
|
+
}
|
|
2390
2454
|
</div>
|
|
2391
2455
|
${generateSuitesWidget(suitesData)}
|
|
2392
2456
|
</div>
|
|
@@ -2396,17 +2460,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2396
2460
|
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2397
2461
|
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
|
|
2398
2462
|
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2463
|
+
new Set(
|
|
2464
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2465
|
+
)
|
|
2466
|
+
)
|
|
2467
|
+
.map(
|
|
2468
|
+
(browser) =>
|
|
2469
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2470
|
+
browser
|
|
2471
|
+
)}</option>`
|
|
2472
|
+
)
|
|
2473
|
+
.join("")}</select>
|
|
2410
2474
|
<button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
2411
2475
|
</div>
|
|
2412
2476
|
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
@@ -2415,16 +2479,18 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2415
2479
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
2416
2480
|
<div class="trend-charts-row">
|
|
2417
2481
|
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2418
|
-
${
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2482
|
+
${
|
|
2483
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2484
|
+
? generateTestTrendsChart(trendData)
|
|
2485
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
2486
|
+
}
|
|
2422
2487
|
</div>
|
|
2423
2488
|
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2424
|
-
${
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2489
|
+
${
|
|
2490
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2491
|
+
? generateDurationTrendChart(trendData)
|
|
2492
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
2493
|
+
}
|
|
2428
2494
|
</div>
|
|
2429
2495
|
</div>
|
|
2430
2496
|
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
@@ -2434,12 +2500,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2434
2500
|
</div>
|
|
2435
2501
|
</div>
|
|
2436
2502
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
2437
|
-
${
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2503
|
+
${
|
|
2504
|
+
trendData &&
|
|
2505
|
+
trendData.testRuns &&
|
|
2506
|
+
Object.keys(trendData.testRuns).length > 0
|
|
2507
|
+
? generateTestHistoryContent(trendData)
|
|
2508
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2509
|
+
}
|
|
2443
2510
|
</div>
|
|
2444
2511
|
<div id="ai-failure-analyzer" class="tab-content">
|
|
2445
2512
|
${generateAIFailureAnalyzerTab(results)}
|
|
@@ -2702,6 +2769,19 @@ function getAIFix(button) {
|
|
|
2702
2769
|
}
|
|
2703
2770
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2704
2771
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2772
|
+
// --- Annotation Link Handler ---
|
|
2773
|
+
document.querySelectorAll('a.annotation-link').forEach(link => {
|
|
2774
|
+
link.addEventListener('click', (e) => {
|
|
2775
|
+
e.preventDefault();
|
|
2776
|
+
const annotationId = link.dataset.annotation;
|
|
2777
|
+
if (annotationId) {
|
|
2778
|
+
const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
|
|
2779
|
+
if (jiraUrl) {
|
|
2780
|
+
window.open(jiraUrl + annotationId, '_blank');
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
});
|
|
2705
2785
|
// --- Intersection Observer for Lazy Loading ---
|
|
2706
2786
|
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
|
|
2707
2787
|
if ('IntersectionObserver' in window) {
|
|
@@ -1939,18 +1939,66 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1939
1939
|
<p><strong>Full Path:</strong> ${sanitizeHTML(
|
|
1940
1940
|
test.name
|
|
1941
1941
|
)}</p>
|
|
1942
|
+
${
|
|
1943
|
+
test.annotations && test.annotations.length > 0
|
|
1944
|
+
? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
|
|
1945
|
+
<h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
|
|
1946
|
+
${test.annotations
|
|
1947
|
+
.map((annotation, idx) => {
|
|
1948
|
+
const isIssueOrBug =
|
|
1949
|
+
annotation.type === "issue" ||
|
|
1950
|
+
annotation.type === "bug";
|
|
1951
|
+
const descriptionText =
|
|
1952
|
+
annotation.description || "";
|
|
1953
|
+
const typeLabel = sanitizeHTML(
|
|
1954
|
+
annotation.type
|
|
1955
|
+
);
|
|
1956
|
+
const descriptionHtml =
|
|
1957
|
+
isIssueOrBug &&
|
|
1958
|
+
descriptionText.match(/^[A-Z]+-\d+$/)
|
|
1959
|
+
? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
|
|
1960
|
+
descriptionText
|
|
1961
|
+
)}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
|
|
1962
|
+
descriptionText
|
|
1963
|
+
)}</a>`
|
|
1964
|
+
: sanitizeHTML(descriptionText);
|
|
1965
|
+
const locationText = annotation.location
|
|
1966
|
+
? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
|
|
1967
|
+
annotation.location.file
|
|
1968
|
+
)}:${annotation.location.line}:${
|
|
1969
|
+
annotation.location.column
|
|
1970
|
+
}</div>`
|
|
1971
|
+
: "";
|
|
1972
|
+
return `<div style="margin-bottom: ${
|
|
1973
|
+
idx < test.annotations.length - 1
|
|
1974
|
+
? "10px"
|
|
1975
|
+
: "0"
|
|
1976
|
+
};">
|
|
1977
|
+
<strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
|
|
1978
|
+
${
|
|
1979
|
+
descriptionText
|
|
1980
|
+
? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
|
|
1981
|
+
: ""
|
|
1982
|
+
}
|
|
1983
|
+
${locationText}
|
|
1984
|
+
</div>`;
|
|
1985
|
+
})
|
|
1986
|
+
.join("")}
|
|
1987
|
+
</div>`
|
|
1988
|
+
: ""
|
|
1989
|
+
}
|
|
1942
1990
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1943
1991
|
test.workerId
|
|
1944
1992
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1945
1993
|
test.totalWorkers
|
|
1946
1994
|
)}]</p>
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1995
|
+
${
|
|
1996
|
+
test.errorMessage
|
|
1997
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1998
|
+
test.errorMessage
|
|
1999
|
+
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
2000
|
+
: ""
|
|
2001
|
+
}
|
|
1954
2002
|
${
|
|
1955
2003
|
test.snippet
|
|
1956
2004
|
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
@@ -1998,7 +2046,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1998
2046
|
test.screenshots.length === 0
|
|
1999
2047
|
)
|
|
2000
2048
|
return "";
|
|
2001
|
-
return `<div class="attachments-section"><h4>Screenshots
|
|
2049
|
+
return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
|
|
2002
2050
|
.map((screenshotPath, index) => {
|
|
2003
2051
|
try {
|
|
2004
2052
|
const imagePath = path.resolve(
|
|
@@ -2840,6 +2888,18 @@ aspect-ratio: 16 / 9;
|
|
|
2840
2888
|
}
|
|
2841
2889
|
return;
|
|
2842
2890
|
}
|
|
2891
|
+
const annotationLink = e.target.closest('a.annotation-link');
|
|
2892
|
+
if (annotationLink) {
|
|
2893
|
+
e.preventDefault();
|
|
2894
|
+
const annotationId = annotationLink.dataset.annotation;
|
|
2895
|
+
if (annotationId) {
|
|
2896
|
+
const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
|
|
2897
|
+
if (jiraUrl) {
|
|
2898
|
+
window.open(jiraUrl + annotationId, '_blank');
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2843
2903
|
const img = e.target.closest('img.lazy-load-image');
|
|
2844
2904
|
if (img && img.dataset && img.dataset.src) {
|
|
2845
2905
|
if (e.preventDefault) e.preventDefault();
|
|
@@ -2870,9 +2930,45 @@ aspect-ratio: 16 / 9;
|
|
|
2870
2930
|
const a = e.target.closest('a.lazy-load-attachment');
|
|
2871
2931
|
if (a && a.dataset && a.dataset.href) {
|
|
2872
2932
|
e.preventDefault();
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
a.
|
|
2933
|
+
|
|
2934
|
+
// Special handling for view-full links to avoid about:blank issue
|
|
2935
|
+
if (a.classList.contains('view-full')) {
|
|
2936
|
+
// Extract the data from the data URI
|
|
2937
|
+
const dataUri = a.dataset.href;
|
|
2938
|
+
const [header, base64Data] = dataUri.split(',');
|
|
2939
|
+
const mimeType = header.match(/data:([^;]+)/)[1];
|
|
2940
|
+
|
|
2941
|
+
try {
|
|
2942
|
+
// Convert base64 to blob
|
|
2943
|
+
const byteCharacters = atob(base64Data);
|
|
2944
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
2945
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
2946
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
2947
|
+
}
|
|
2948
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
2949
|
+
const blob = new Blob([byteArray], { type: mimeType });
|
|
2950
|
+
|
|
2951
|
+
// Create a URL and open it
|
|
2952
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
2953
|
+
const newWindow = window.open(blobUrl, '_blank');
|
|
2954
|
+
|
|
2955
|
+
// Clean up the URL after a delay
|
|
2956
|
+
setTimeout(() => {
|
|
2957
|
+
URL.revokeObjectURL(blobUrl);
|
|
2958
|
+
}, 1000);
|
|
2959
|
+
} catch (error) {
|
|
2960
|
+
console.error('Failed to open attachment:', error);
|
|
2961
|
+
// Fallback to original method
|
|
2962
|
+
a.href = a.dataset.href;
|
|
2963
|
+
a.removeAttribute('data-href');
|
|
2964
|
+
a.click();
|
|
2965
|
+
}
|
|
2966
|
+
} else {
|
|
2967
|
+
// For download links, use the original method
|
|
2968
|
+
a.href = a.dataset.href;
|
|
2969
|
+
a.removeAttribute('data-href');
|
|
2970
|
+
a.click();
|
|
2971
|
+
}
|
|
2876
2972
|
return;
|
|
2877
2973
|
}
|
|
2878
2974
|
});
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -289,7 +289,7 @@ const sendEmail = async (credentials) => {
|
|
|
289
289
|
}
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
async function fetchCredentials(retries =
|
|
292
|
+
async function fetchCredentials(retries = 10) {
|
|
293
293
|
// Ensure fetch is initialized from the dynamic import before calling this
|
|
294
294
|
if (!fetch) {
|
|
295
295
|
try {
|
|
@@ -324,7 +324,7 @@ async function fetchCredentials(retries = 6) {
|
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
const fetchPromise = fetch(
|
|
327
|
-
"https://
|
|
327
|
+
"https://get-credentials.netlify.app/api/getcredentials",
|
|
328
328
|
{
|
|
329
329
|
method: "GET",
|
|
330
330
|
headers: {
|