@testrelic/playwright-analytics 1.0.0 → 1.1.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/dist/cli.cjs +3 -1
- package/dist/fixture.cjs +10 -1
- package/dist/fixture.cjs.map +1 -1
- package/dist/fixture.js +10 -1
- package/dist/fixture.js.map +1 -1
- package/dist/index.cjs +283 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +279 -26
- package/dist/index.js.map +1 -1
- package/dist/merge.cjs +3 -1
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.js +3 -1
- package/dist/merge.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -28,8 +28,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
|
|
29
29
|
// src/reporter.ts
|
|
30
30
|
var import_node_crypto = require("crypto");
|
|
31
|
-
var
|
|
32
|
-
var
|
|
31
|
+
var import_node_fs4 = require("fs");
|
|
32
|
+
var import_node_path3 = require("path");
|
|
33
33
|
|
|
34
34
|
// src/config.ts
|
|
35
35
|
var import_core = require("@testrelic/core");
|
|
@@ -59,11 +59,12 @@ function resolveConfig(options) {
|
|
|
59
59
|
const outputPath = target.outputPath;
|
|
60
60
|
target.openReport = options?.openReport ?? true;
|
|
61
61
|
target.htmlReportPath = options?.htmlReportPath ?? outputPath.replace(/\.json$/, ".html");
|
|
62
|
+
target.includeArtifacts = options?.includeArtifacts ?? true;
|
|
62
63
|
return Object.freeze(target);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// src/schema.ts
|
|
66
|
-
var SCHEMA_VERSION = "1.
|
|
67
|
+
var SCHEMA_VERSION = "1.2.0";
|
|
67
68
|
|
|
68
69
|
// src/code-extractor.ts
|
|
69
70
|
var import_node_fs = require("fs");
|
|
@@ -203,6 +204,8 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
203
204
|
.timeline-entry{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;
|
|
204
205
|
border-left:4px solid #e5e7eb;transition:border-color .15s}
|
|
205
206
|
.timeline-entry:hover{border-color:#d1d5db}
|
|
207
|
+
.timeline-entry--all-passed{border-left-color:#22c55e}
|
|
208
|
+
.timeline-entry--all-passed:hover{border-color:#86efac}
|
|
206
209
|
.timeline-entry--has-failures{border-left-color:#ef4444}
|
|
207
210
|
.timeline-entry--has-failures:hover{border-color:#fca5a5}
|
|
208
211
|
.timeline-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;
|
|
@@ -214,13 +217,22 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
214
217
|
text-overflow:ellipsis;white-space:nowrap;max-width:500px;min-width:0}
|
|
215
218
|
.nav-type-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:9999px;
|
|
216
219
|
white-space:nowrap;text-transform:uppercase;letter-spacing:.02em;flex-shrink:0}
|
|
217
|
-
.badge-goto
|
|
218
|
-
.badge-
|
|
219
|
-
.badge-
|
|
220
|
-
.badge-
|
|
220
|
+
.badge-goto{background:#dbeafe;color:#1e40af}
|
|
221
|
+
.badge-navigation{background:#e0e7ff;color:#3730a3}
|
|
222
|
+
.badge-page_load{background:#c7d2fe;color:#312e81}
|
|
223
|
+
.badge-back{background:#cffafe;color:#155e75}
|
|
224
|
+
.badge-forward{background:#a5f3fc;color:#164e63}
|
|
225
|
+
.badge-spa_route{background:#ede9fe;color:#5b21b6}
|
|
226
|
+
.badge-spa_replace{background:#ddd6fe;color:#4c1d95}
|
|
227
|
+
.badge-hash_change{background:#fae8ff;color:#86198f}
|
|
228
|
+
.badge-popstate{background:#f5d0fe;color:#701a75}
|
|
229
|
+
.badge-link_click{background:#fce7f3;color:#9d174d}
|
|
230
|
+
.badge-form_submit{background:#fbcfe8;color:#831843}
|
|
221
231
|
.badge-redirect{background:#ffedd5;color:#9a3412}
|
|
222
232
|
.badge-refresh{background:#e0f2fe;color:#075985}
|
|
223
|
-
.badge-dummy
|
|
233
|
+
.badge-dummy{background:#f3f4f6;color:#6b7280}
|
|
234
|
+
.badge-fallback{background:#e5e7eb;color:#4b5563}
|
|
235
|
+
.badge-manual_record{background:#fef9c3;color:#854d0e}
|
|
224
236
|
.timeline-info{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b7280;
|
|
225
237
|
margin-left:auto;flex-shrink:0;white-space:nowrap}
|
|
226
238
|
.spec-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
|
|
@@ -232,19 +244,38 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
232
244
|
.spec-summary{font-size:12px;color:#6b7280;margin-bottom:8px;display:flex;gap:12px;flex-wrap:wrap}
|
|
233
245
|
.spec-summary span{font-weight:600}
|
|
234
246
|
.spec-passed{color:#16a34a}.spec-failed{color:#dc2626}.spec-flaky{color:#d97706}
|
|
235
|
-
.test-card{
|
|
236
|
-
border-bottom:1px solid #f3f4f6}
|
|
247
|
+
.test-card{padding:8px 0;border-bottom:1px solid #f3f4f6}
|
|
237
248
|
.test-card:last-child{border-bottom:none}
|
|
249
|
+
.test-card-row{display:flex;align-items:flex-start;gap:8px}
|
|
238
250
|
.status-badge{font-size:11px;font-weight:700;padding:2px 8px;border-radius:9999px;
|
|
239
251
|
flex-shrink:0;margin-top:2px;text-transform:uppercase;letter-spacing:.03em}
|
|
240
252
|
.status-passed{background:#d1fae5;color:#065f46}
|
|
241
253
|
.status-failed{background:#fecaca;color:#991b1b}
|
|
242
254
|
.status-flaky{background:#fde68a;color:#92400e}
|
|
255
|
+
.status-skipped{background:#f3f4f6;color:#6b7280}
|
|
256
|
+
.status-timedout{background:#ffedd5;color:#9a3412}
|
|
257
|
+
.type-badge{background:#ede9fe;color:#5b21b6;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
258
|
+
.flaky-badge{background:#fef3c7;color:#92400e;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
259
|
+
.file-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:#6b7280}
|
|
260
|
+
.suite-name{font-size:11px;color:#9ca3af}
|
|
261
|
+
.status-mismatch{font-size:10px;color:#dc2626;font-weight:600}
|
|
262
|
+
.card-timedout{border-color:#ffedd5;background:#fff7ed}.card-timedout .count{color:#ea580c}
|
|
243
263
|
.test-info{flex:1;min-width:0}
|
|
244
264
|
.test-title{font-size:13px;color:#171717;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
245
265
|
.test-meta{display:flex;gap:8px;align-items:center;margin-top:2px;font-size:11px;color:#9ca3af}
|
|
246
266
|
.retry-badge{background:#fef3c7;color:#92400e;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
247
267
|
.tag-badge{background:#eff6ff;color:#1d4ed8;padding:1px 5px;border-radius:3px;font-size:10px}
|
|
268
|
+
.meta-prefix{font-size:10px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
|
269
|
+
.test-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
|
|
270
|
+
color:#6b7280;background:#f3f4f6;padding:1px 6px;border-radius:3px}
|
|
271
|
+
.network-panel--has-failures{border-color:#fecaca}
|
|
272
|
+
.network-panel--has-failures .network-header{background:#fef2f2;color:#991b1b}
|
|
273
|
+
.failed-requests-banner{display:flex;align-items:center;gap:6px;padding:8px 12px;
|
|
274
|
+
background:#fef2f2;color:#991b1b;font-size:12px;font-weight:600;border-top:1px solid #fecaca}
|
|
275
|
+
.failed-urls-list{padding:6px 12px;background:#fef2f2;border-top:1px solid #fecaca;font-size:11px;
|
|
276
|
+
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;max-height:200px;overflow-y:auto}
|
|
277
|
+
.failed-url-item{padding:2px 0;color:#991b1b;word-break:break-all}
|
|
278
|
+
.failed-url-status{font-weight:700;margin-right:4px}
|
|
248
279
|
.test-duration{font-size:12px;color:#9ca3af;flex-shrink:0;margin-top:2px}
|
|
249
280
|
/* Failure Details */
|
|
250
281
|
.failure-toggle{font-size:12px;color:#03b79c;cursor:pointer;margin-top:4px;
|
|
@@ -295,6 +326,34 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
295
326
|
.section-title{font-size:16px;font-weight:700;color:#171717;margin-bottom:12px;
|
|
296
327
|
display:flex;align-items:center;gap:8px}
|
|
297
328
|
.section-title::before{content:'';width:4px;height:18px;background:#03b79c;border-radius:2px}
|
|
329
|
+
/* Artifacts Panel */
|
|
330
|
+
.artifacts-panel{margin-top:10px;padding:10px 12px;background:#f9fafb;border:1px solid #e5e7eb;
|
|
331
|
+
border-radius:8px}
|
|
332
|
+
.artifacts-panel-header{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
|
|
333
|
+
color:#6b7280;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
|
|
334
|
+
.artifacts-row{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap}
|
|
335
|
+
/* Screenshot */
|
|
336
|
+
.artifact-screenshot-wrap{display:flex;flex-direction:column;gap:4px;flex-shrink:0}
|
|
337
|
+
.artifact-thumbnail{height:140px;width:auto;border-radius:6px;cursor:pointer;
|
|
338
|
+
border:1px solid #e5e7eb;transition:border-color .15s,box-shadow .15s;object-fit:cover;display:block}
|
|
339
|
+
.artifact-thumbnail:hover{border-color:#03b79c;box-shadow:0 0 0 2px rgba(3,183,156,.2)}
|
|
340
|
+
.artifact-label{font-size:10px;color:#9ca3af;text-align:center}
|
|
341
|
+
/* Video */
|
|
342
|
+
.artifact-video-wrap{flex:1;min-width:200px;max-width:480px;display:flex;flex-direction:column;gap:4px}
|
|
343
|
+
.artifact-video-container{border-radius:6px;overflow:hidden;border:1px solid #e5e7eb;display:none}
|
|
344
|
+
.artifact-video-container.show{display:block}
|
|
345
|
+
.artifact-video{width:100%;max-height:320px;display:block;background:#000}
|
|
346
|
+
.video-toggle{font-size:12px;color:#03b79c;cursor:pointer;
|
|
347
|
+
border:none;background:none;padding:0;text-decoration:underline;font-family:inherit}
|
|
348
|
+
.video-toggle:hover{color:#028a75}
|
|
349
|
+
/* Lightbox */
|
|
350
|
+
.lightbox-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:1000;
|
|
351
|
+
display:flex;align-items:center;justify-content:center;cursor:zoom-out}
|
|
352
|
+
.lightbox-overlay img{max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.4)}
|
|
353
|
+
.lightbox-close{position:fixed;top:16px;right:16px;z-index:1001;width:36px;height:36px;
|
|
354
|
+
border-radius:50%;border:none;background:rgba(255,255,255,.15);color:#fff;font-size:20px;
|
|
355
|
+
cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s}
|
|
356
|
+
.lightbox-close:hover{background:rgba(255,255,255,.3)}
|
|
298
357
|
`;
|
|
299
358
|
var JS = `
|
|
300
359
|
(function(){
|
|
@@ -339,6 +398,7 @@ var JS = `
|
|
|
339
398
|
h+='<div class="summary-card card-failed"><div class="count">'+s.failed+'</div><div class="label">Failed</div></div>';
|
|
340
399
|
h+='<div class="summary-card card-flaky"><div class="count">'+s.flaky+'</div><div class="label">Flaky</div></div>';
|
|
341
400
|
h+='<div class="summary-card card-skipped"><div class="count">'+s.skipped+'</div><div class="label">Skipped</div></div>';
|
|
401
|
+
if(s.timedout!==undefined){h+='<div class="summary-card card-timedout"><div class="count">'+s.timedout+'</div><div class="label">Timed Out</div></div>';}
|
|
342
402
|
h+='</div>';
|
|
343
403
|
|
|
344
404
|
// Timeline
|
|
@@ -350,7 +410,8 @@ var JS = `
|
|
|
350
410
|
for(var i=0;i<data.timeline.length;i++){
|
|
351
411
|
var e=data.timeline[i];
|
|
352
412
|
var hasFail=e.tests.some(function(t){return t.status==='failed'});
|
|
353
|
-
var
|
|
413
|
+
var allPassed=!hasFail&&e.tests.length>0&&e.tests.every(function(t){return t.status==='passed'||t.status==='flaky'});
|
|
414
|
+
var cls='timeline-entry'+(hasFail?' timeline-entry--has-failures':'')+(allPassed?' timeline-entry--all-passed':'');
|
|
354
415
|
h+='<div class="'+cls+'" data-idx="'+i+'">';
|
|
355
416
|
h+='<div class="timeline-header" onclick="toggleEntry(this)">';
|
|
356
417
|
h+='<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
|
|
@@ -375,23 +436,35 @@ var JS = `
|
|
|
375
436
|
if(!tests||tests.length===0)return '';
|
|
376
437
|
var out='<div class="tests-section">';
|
|
377
438
|
// Spec summary
|
|
378
|
-
var pc=0,fc=0,fkc=0;
|
|
379
|
-
tests.forEach(function(t){if(t.status==='passed')pc++;else if(t.status==='failed')fc++;else if(t.status==='flaky')fkc++});
|
|
439
|
+
var pc=0,fc=0,fkc=0,sc=0,toc=0;
|
|
440
|
+
tests.forEach(function(t){if(t.status==='passed')pc++;else if(t.status==='failed')fc++;else if(t.status==='flaky')fkc++;else if(t.status==='skipped')sc++;else if(t.status==='timedout')toc++});
|
|
380
441
|
out+='<div class="spec-summary">';
|
|
381
442
|
if(pc>0)out+='<span class="spec-passed">'+pc+' passed</span>';
|
|
382
443
|
if(fc>0)out+='<span class="spec-failed">'+fc+' failed</span>';
|
|
383
444
|
if(fkc>0)out+='<span class="spec-flaky">'+fkc+' flaky</span>';
|
|
445
|
+
if(sc>0)out+='<span style="color:#6b7280;font-weight:600">'+sc+' skipped</span>';
|
|
446
|
+
if(toc>0)out+='<span style="color:#ea580c;font-weight:600">'+toc+' timed out</span>';
|
|
384
447
|
out+='</div>';
|
|
385
448
|
for(var j=0;j<tests.length;j++){
|
|
386
449
|
var t=tests[j];
|
|
387
450
|
out+='<div class="test-card">';
|
|
451
|
+
out+='<div class="test-card-row">';
|
|
388
452
|
out+='<span class="status-badge status-'+t.status+'">'+t.status+'</span>';
|
|
389
453
|
out+='<div class="test-info">';
|
|
390
454
|
out+='<div class="test-title" title="'+esc(t.title)+'">'+esc(t.title)+'</div>';
|
|
391
455
|
out+='<div class="test-meta">';
|
|
456
|
+
if(t.testType&&t.testType!=='unknown')out+='<span class="type-badge">'+esc(t.testType)+'</span>';
|
|
457
|
+
if(t.isFlaky)out+='<span class="flaky-badge">flaky</span>';
|
|
392
458
|
if(t.retryCount>0)out+='<span class="retry-badge">'+t.retryCount+' retries</span>';
|
|
393
|
-
if(t.
|
|
459
|
+
if(t.retryStatus)out+='<span class="retry-badge">'+esc(t.retryStatus)+'</span>';
|
|
460
|
+
if(t.tags&&t.tags.length>0){out+='<span class="meta-prefix">Tags:</span>';t.tags.forEach(function(tag){if(tag)out+='<span class="tag-badge">'+esc(tag)+'</span>'});}
|
|
461
|
+
if(t.expectedStatus&&t.actualStatus&&t.expectedStatus!==t.actualStatus)out+='<span class="status-mismatch">expected: '+esc(t.expectedStatus)+' actual: '+esc(t.actualStatus)+'</span>';
|
|
394
462
|
out+='</div>';
|
|
463
|
+
if(t.testId||t.filePath||t.suiteName){out+='<div class="test-meta">';
|
|
464
|
+
if(t.testId)out+='<span class="meta-prefix">ID:</span><span class="test-id" title="Full ID: '+esc(t.testId)+'">'+esc(t.testId.substring(0,12))+'</span>';
|
|
465
|
+
if(t.filePath)out+='<span class="meta-prefix">File:</span><span class="file-path">'+esc(t.filePath)+'</span>';
|
|
466
|
+
if(t.suiteName)out+='<span class="meta-prefix">Suite:</span><span class="suite-name">'+esc(t.suiteName)+'</span>';
|
|
467
|
+
out+='</div>';}
|
|
395
468
|
if((t.status==='failed'||t.status==='flaky')&&t.failure){
|
|
396
469
|
var fid='fail-'+Math.random().toString(36).substr(2,9);
|
|
397
470
|
out+='<button class="failure-toggle" onclick="toggleFail(\\''+fid+'\\')">Show details</button>';
|
|
@@ -415,6 +488,26 @@ var JS = `
|
|
|
415
488
|
out+='</div>';
|
|
416
489
|
out+='<span class="test-duration">'+fmtDur(t.duration)+'</span>';
|
|
417
490
|
out+='</div>';
|
|
491
|
+
if(t.artifacts&&(t.artifacts.screenshot||t.artifacts.video)){
|
|
492
|
+
out+='<div class="artifacts-panel">';
|
|
493
|
+
out+='<div class="artifacts-panel-header"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>Artifacts</div>';
|
|
494
|
+
out+='<div class="artifacts-row">';
|
|
495
|
+
if(t.artifacts.screenshot){
|
|
496
|
+
out+='<div class="artifact-screenshot-wrap">';
|
|
497
|
+
out+='<img class="artifact-thumbnail" src="'+esc(t.artifacts.screenshot)+'" loading="lazy" alt="Screenshot" onclick="openLightbox(this.src)">';
|
|
498
|
+
out+='<span class="artifact-label">Click to enlarge</span>';
|
|
499
|
+
out+='</div>';
|
|
500
|
+
}
|
|
501
|
+
if(t.artifacts.video){
|
|
502
|
+
var vid='video-'+Math.random().toString(36).substr(2,9);
|
|
503
|
+
out+='<div class="artifact-video-wrap">';
|
|
504
|
+
out+='<button class="video-toggle" onclick="toggleVideo(\\''+vid+'\\')">Show video</button>';
|
|
505
|
+
out+='<div class="artifact-video-container" id="'+vid+'"><video class="artifact-video" controls preload="none" src="'+esc(t.artifacts.video)+'"></video></div>';
|
|
506
|
+
out+='</div>';
|
|
507
|
+
}
|
|
508
|
+
out+='</div></div>';
|
|
509
|
+
}
|
|
510
|
+
out+='</div>';
|
|
418
511
|
}
|
|
419
512
|
out+='</div>';
|
|
420
513
|
return out;
|
|
@@ -440,13 +533,17 @@ var JS = `
|
|
|
440
533
|
|
|
441
534
|
function renderNetwork(stats){
|
|
442
535
|
if(!stats)return '<div class="no-network">No network data captured</div>';
|
|
443
|
-
var
|
|
536
|
+
var hasFailedReqs=stats.failedRequests>0;
|
|
537
|
+
var out='<div class="network-panel'+(hasFailedReqs?' network-panel--has-failures':'')+'">';
|
|
444
538
|
out+='<div class="network-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>Network</div>';
|
|
445
539
|
out+='<div class="network-summary">';
|
|
446
540
|
out+='<div class="net-stat"><span class="val">'+stats.totalRequests+'</span><span class="lbl">Requests</span></div>';
|
|
447
|
-
out+='<div class="net-stat'+(
|
|
541
|
+
out+='<div class="net-stat'+(hasFailedReqs?' has-failed':'')+'"><span class="val">'+stats.failedRequests+'</span><span class="lbl">Failed</span></div>';
|
|
448
542
|
out+='<div class="net-stat"><span class="val">'+fmtBytes(stats.totalBytes)+'</span><span class="lbl">Transferred</span></div>';
|
|
449
543
|
out+='</div>';
|
|
544
|
+
if(hasFailedReqs){out+='<div class="failed-requests-banner"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v2m0 4h.01M10.29 3.86l-8.58 14.6A2 2 0 003.44 21h17.12a2 2 0 001.73-2.54l-8.58-14.6a2 2 0 00-3.42 0z"/></svg>'+stats.failedRequests+' network request'+(stats.failedRequests>1?'s':'')+' failed</div>';
|
|
545
|
+
if(stats.failedRequestUrls&&stats.failedRequestUrls.length>0){out+='<div class="failed-urls-list">';for(var fi=0;fi<stats.failedRequestUrls.length;fi++){var furl=stats.failedRequestUrls[fi];var spIdx=furl.indexOf(' ');var fStatus=spIdx>0?furl.substring(0,spIdx):'';var fPath=spIdx>0?furl.substring(spIdx+1):furl;out+='<div class="failed-url-item"><span class="failed-url-status">'+esc(fStatus)+'</span>'+esc(fPath)+'</div>';}out+='</div>';}
|
|
546
|
+
}
|
|
450
547
|
if(stats.byType){
|
|
451
548
|
var types=[['XHR','xhr'],['Document','document'],['Script','script'],['Stylesheet','stylesheet'],['Image','image'],['Font','font'],['Other','other']];
|
|
452
549
|
out+='<div class="resource-breakdown">';
|
|
@@ -465,6 +562,21 @@ var JS = `
|
|
|
465
562
|
function toggleEntry(el){el.closest('.timeline-entry').classList.toggle('expanded')}
|
|
466
563
|
function toggleFail(id){document.getElementById(id).classList.toggle('show')}
|
|
467
564
|
function toggleStack(id){document.getElementById(id).classList.toggle('show')}
|
|
565
|
+
function openLightbox(src){var o=document.createElement('div');o.className='lightbox-overlay';
|
|
566
|
+
o.onclick=function(){closeLightbox()};o.innerHTML='<img src="'+src.replace(/"/g,'"')+'" alt="Screenshot">';
|
|
567
|
+
var b=document.createElement('button');b.className='lightbox-close';b.innerHTML='×';
|
|
568
|
+
b.onclick=function(e){e.stopPropagation();closeLightbox()};
|
|
569
|
+
document.body.appendChild(o);document.body.appendChild(b);
|
|
570
|
+
document.addEventListener('keydown',lightboxEsc)}
|
|
571
|
+
function closeLightbox(){var o=document.querySelector('.lightbox-overlay');if(o)o.remove();
|
|
572
|
+
var b=document.querySelector('.lightbox-close');if(b)b.remove();
|
|
573
|
+
document.removeEventListener('keydown',lightboxEsc)}
|
|
574
|
+
function lightboxEsc(e){if(e.key==='Escape')closeLightbox()}
|
|
575
|
+
function toggleVideo(id){var el=document.getElementById(id);if(!el)return;
|
|
576
|
+
var isShown=el.classList.toggle('show');
|
|
577
|
+
var btn=el.previousElementSibling;
|
|
578
|
+
if(btn)btn.textContent=isShown?'Hide video':'Show video';
|
|
579
|
+
if(!isShown){var v=el.querySelector('video');if(v)v.pause()}}
|
|
468
580
|
`;
|
|
469
581
|
function renderHtmlDocument(reportJson) {
|
|
470
582
|
const jsWithLogo = JS.replace(/__LOGO_SVG__/g, LOGO_SVG.replace(/'/g, "\\'").replace(/\n/g, ""));
|
|
@@ -474,7 +586,7 @@ function renderHtmlDocument(reportJson) {
|
|
|
474
586
|
<head>
|
|
475
587
|
<meta charset="UTF-8">
|
|
476
588
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
477
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data
|
|
589
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: 'self'; media-src blob: 'self';">
|
|
478
590
|
<title>TestRelic Report</title>
|
|
479
591
|
<style>${CSS}</style>
|
|
480
592
|
</head>
|
|
@@ -537,7 +649,108 @@ function writeHtmlReport(report, config) {
|
|
|
537
649
|
}
|
|
538
650
|
}
|
|
539
651
|
|
|
652
|
+
// src/artifact-manager.ts
|
|
653
|
+
var import_node_fs3 = require("fs");
|
|
654
|
+
var import_node_path2 = require("path");
|
|
655
|
+
function sanitizeFolderName(title) {
|
|
656
|
+
let name = title.replace(/[^a-zA-Z0-9\-_ ]/g, "-").replace(/\s+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
657
|
+
if (name.length > 100) {
|
|
658
|
+
name = name.substring(0, 100).replace(/-+$/, "");
|
|
659
|
+
}
|
|
660
|
+
return name || "unnamed-test";
|
|
661
|
+
}
|
|
662
|
+
function copyArtifacts(attachments, testTitle, retryCount, outputDir) {
|
|
663
|
+
const screenshot = attachments.find(
|
|
664
|
+
(a) => a.name === "screenshot" && a.path
|
|
665
|
+
);
|
|
666
|
+
const video = attachments.find(
|
|
667
|
+
(a) => a.name === "video" && a.path
|
|
668
|
+
);
|
|
669
|
+
if (!screenshot && !video) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
let folderName = sanitizeFolderName(testTitle);
|
|
673
|
+
if (retryCount > 0) {
|
|
674
|
+
folderName += `--retry-${retryCount}`;
|
|
675
|
+
}
|
|
676
|
+
const artifactDir = (0, import_node_path2.join)(outputDir, "artifacts", folderName);
|
|
677
|
+
const result = {};
|
|
678
|
+
try {
|
|
679
|
+
(0, import_node_fs3.mkdirSync)(artifactDir, { recursive: true });
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
if (screenshot?.path) {
|
|
684
|
+
try {
|
|
685
|
+
if ((0, import_node_fs3.existsSync)(screenshot.path)) {
|
|
686
|
+
const ext = (0, import_node_path2.extname)(screenshot.path) || ".png";
|
|
687
|
+
const destName = `screenshot${ext}`;
|
|
688
|
+
(0, import_node_fs3.copyFileSync)(screenshot.path, (0, import_node_path2.join)(artifactDir, destName));
|
|
689
|
+
result.screenshot = `artifacts/${folderName}/${destName}`;
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (video?.path) {
|
|
695
|
+
try {
|
|
696
|
+
if ((0, import_node_fs3.existsSync)(video.path)) {
|
|
697
|
+
const ext = (0, import_node_path2.extname)(video.path) || ".webm";
|
|
698
|
+
const destName = `video${ext}`;
|
|
699
|
+
(0, import_node_fs3.copyFileSync)(video.path, (0, import_node_path2.join)(artifactDir, destName));
|
|
700
|
+
result.video = `artifacts/${folderName}/${destName}`;
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!result.screenshot && !result.video) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
|
|
540
711
|
// src/reporter.ts
|
|
712
|
+
function mapPlaywrightStatus(status) {
|
|
713
|
+
switch (status) {
|
|
714
|
+
case "passed":
|
|
715
|
+
return "passed";
|
|
716
|
+
case "failed":
|
|
717
|
+
return "failed";
|
|
718
|
+
case "timedOut":
|
|
719
|
+
return "timedout";
|
|
720
|
+
case "skipped":
|
|
721
|
+
return "skipped";
|
|
722
|
+
case "interrupted":
|
|
723
|
+
return "failed";
|
|
724
|
+
default:
|
|
725
|
+
return "failed";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function generateTestId(filePath, suiteName, title) {
|
|
729
|
+
const input = `${filePath}::${suiteName}::${title}`;
|
|
730
|
+
return (0, import_node_crypto.createHash)("sha256").update(input).digest("hex").substring(0, 16);
|
|
731
|
+
}
|
|
732
|
+
function getSuiteName(titlePath) {
|
|
733
|
+
if (titlePath.length <= 4) return "";
|
|
734
|
+
return titlePath[titlePath.length - 2];
|
|
735
|
+
}
|
|
736
|
+
function getRetryStatus(results) {
|
|
737
|
+
const passedIndex = results.findIndex((r) => r.status === "passed");
|
|
738
|
+
return passedIndex > 0 ? `passed on retry ${passedIndex}` : null;
|
|
739
|
+
}
|
|
740
|
+
function getTestType(tags, filePath) {
|
|
741
|
+
const typeOrder = ["e2e", "api", "unit"];
|
|
742
|
+
for (const type of typeOrder) {
|
|
743
|
+
if (tags.some((tag) => tag === `@${type}` || tag === type)) {
|
|
744
|
+
return type;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const type of typeOrder) {
|
|
748
|
+
if (filePath.includes(`/${type}/`)) {
|
|
749
|
+
return type;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return "unknown";
|
|
753
|
+
}
|
|
541
754
|
var TestRelicReporter = class {
|
|
542
755
|
constructor(options) {
|
|
543
756
|
this.rootDir = "";
|
|
@@ -561,12 +774,11 @@ var TestRelicReporter = class {
|
|
|
561
774
|
let status;
|
|
562
775
|
if (outcome === "flaky") {
|
|
563
776
|
status = "flaky";
|
|
564
|
-
} else if (
|
|
565
|
-
status = "
|
|
777
|
+
} else if (outcome === "skipped") {
|
|
778
|
+
status = "skipped";
|
|
566
779
|
} else {
|
|
567
|
-
status =
|
|
780
|
+
status = mapPlaywrightStatus(lastResult.status);
|
|
568
781
|
}
|
|
569
|
-
if (outcome === "skipped") return;
|
|
570
782
|
const startedAt = lastResult.startTime.toISOString();
|
|
571
783
|
const completedAt = new Date(lastResult.startTime.getTime() + lastResult.duration).toISOString();
|
|
572
784
|
const tags = test.tags ? [...test.tags] : test.annotations.filter((a) => a.type === "tag").map((a) => a.description ?? "");
|
|
@@ -602,10 +814,25 @@ var TestRelicReporter = class {
|
|
|
602
814
|
}
|
|
603
815
|
}
|
|
604
816
|
const titlePath = test.titlePath().filter(Boolean);
|
|
605
|
-
const specFile = (0,
|
|
817
|
+
const specFile = (0, import_node_path3.relative)(this.rootDir || ".", test.location.file);
|
|
818
|
+
const suiteName = getSuiteName(titlePath);
|
|
819
|
+
const title = titlePath.join(" > ");
|
|
820
|
+
const filePath = specFile;
|
|
821
|
+
const testId = generateTestId(filePath, suiteName, title);
|
|
822
|
+
const testType = getTestType(tags, filePath);
|
|
823
|
+
const isFlaky = outcome === "flaky";
|
|
824
|
+
const retryStatus = getRetryStatus(test.results);
|
|
825
|
+
const expectedStatus = mapPlaywrightStatus(test.expectedStatus);
|
|
826
|
+
const actualStatus = mapPlaywrightStatus(lastResult.status);
|
|
827
|
+
let artifacts = null;
|
|
828
|
+
if (this.config.includeArtifacts && status !== "skipped" && lastResult.attachments) {
|
|
829
|
+
const outputDir = (0, import_node_path3.dirname)(this.config.outputPath);
|
|
830
|
+
const lastTitle = titlePath[titlePath.length - 1] ?? test.title;
|
|
831
|
+
artifacts = copyArtifacts(lastResult.attachments, lastTitle, lastResult.retry, outputDir);
|
|
832
|
+
}
|
|
606
833
|
this.collectedTests.push({
|
|
607
834
|
titlePath,
|
|
608
|
-
title
|
|
835
|
+
title,
|
|
609
836
|
status,
|
|
610
837
|
duration: lastResult.duration,
|
|
611
838
|
startedAt,
|
|
@@ -614,7 +841,16 @@ var TestRelicReporter = class {
|
|
|
614
841
|
tags,
|
|
615
842
|
failure,
|
|
616
843
|
specFile,
|
|
617
|
-
navigations
|
|
844
|
+
navigations,
|
|
845
|
+
testId,
|
|
846
|
+
filePath,
|
|
847
|
+
suiteName,
|
|
848
|
+
testType,
|
|
849
|
+
isFlaky,
|
|
850
|
+
retryStatus,
|
|
851
|
+
expectedStatus,
|
|
852
|
+
actualStatus,
|
|
853
|
+
artifacts
|
|
618
854
|
});
|
|
619
855
|
} catch {
|
|
620
856
|
}
|
|
@@ -694,6 +930,7 @@ var TestRelicReporter = class {
|
|
|
694
930
|
let failed = 0;
|
|
695
931
|
let flaky = 0;
|
|
696
932
|
let skipped = 0;
|
|
933
|
+
let timedout = 0;
|
|
697
934
|
for (const test of this.collectedTests) {
|
|
698
935
|
switch (test.status) {
|
|
699
936
|
case "passed":
|
|
@@ -705,6 +942,12 @@ var TestRelicReporter = class {
|
|
|
705
942
|
case "flaky":
|
|
706
943
|
flaky++;
|
|
707
944
|
break;
|
|
945
|
+
case "skipped":
|
|
946
|
+
skipped++;
|
|
947
|
+
break;
|
|
948
|
+
case "timedout":
|
|
949
|
+
timedout++;
|
|
950
|
+
break;
|
|
708
951
|
}
|
|
709
952
|
}
|
|
710
953
|
return {
|
|
@@ -712,7 +955,8 @@ var TestRelicReporter = class {
|
|
|
712
955
|
passed,
|
|
713
956
|
failed,
|
|
714
957
|
flaky,
|
|
715
|
-
skipped
|
|
958
|
+
skipped,
|
|
959
|
+
timedout
|
|
716
960
|
};
|
|
717
961
|
}
|
|
718
962
|
toTestResult(test) {
|
|
@@ -724,18 +968,27 @@ var TestRelicReporter = class {
|
|
|
724
968
|
completedAt: test.completedAt,
|
|
725
969
|
retryCount: test.retryCount,
|
|
726
970
|
tags: test.tags,
|
|
727
|
-
failure: test.failure
|
|
971
|
+
failure: test.failure,
|
|
972
|
+
testId: test.testId,
|
|
973
|
+
filePath: test.filePath,
|
|
974
|
+
suiteName: test.suiteName,
|
|
975
|
+
testType: test.testType,
|
|
976
|
+
isFlaky: test.isFlaky,
|
|
977
|
+
retryStatus: test.retryStatus,
|
|
978
|
+
expectedStatus: test.expectedStatus,
|
|
979
|
+
actualStatus: test.actualStatus,
|
|
980
|
+
artifacts: test.artifacts
|
|
728
981
|
};
|
|
729
982
|
}
|
|
730
983
|
writeReport(report) {
|
|
731
984
|
try {
|
|
732
985
|
const json = JSON.stringify(report, null, 2);
|
|
733
986
|
const outputPath = this.config.outputPath;
|
|
734
|
-
const dir = (0,
|
|
735
|
-
(0,
|
|
987
|
+
const dir = (0, import_node_path3.dirname)(outputPath);
|
|
988
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
736
989
|
const tmpPath = outputPath + ".tmp";
|
|
737
|
-
(0,
|
|
738
|
-
(0,
|
|
990
|
+
(0, import_node_fs4.writeFileSync)(tmpPath, json, "utf-8");
|
|
991
|
+
(0, import_node_fs4.renameSync)(tmpPath, outputPath);
|
|
739
992
|
} catch (err) {
|
|
740
993
|
process.stderr.write(
|
|
741
994
|
`[testrelic] Failed to write report: ${err instanceof Error ? err.message : String(err)}
|