@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/reporter.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { mkdirSync as
|
|
2
|
+
import { randomUUID, createHash } from "crypto";
|
|
3
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "fs";
|
|
4
4
|
import { dirname as dirname2, relative } from "path";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
@@ -31,11 +31,12 @@ function resolveConfig(options) {
|
|
|
31
31
|
const outputPath = target.outputPath;
|
|
32
32
|
target.openReport = options?.openReport ?? true;
|
|
33
33
|
target.htmlReportPath = options?.htmlReportPath ?? outputPath.replace(/\.json$/, ".html");
|
|
34
|
+
target.includeArtifacts = options?.includeArtifacts ?? true;
|
|
34
35
|
return Object.freeze(target);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// src/schema.ts
|
|
38
|
-
var SCHEMA_VERSION = "1.
|
|
39
|
+
var SCHEMA_VERSION = "1.2.0";
|
|
39
40
|
|
|
40
41
|
// src/code-extractor.ts
|
|
41
42
|
import { readFileSync } from "fs";
|
|
@@ -175,6 +176,8 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
175
176
|
.timeline-entry{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;
|
|
176
177
|
border-left:4px solid #e5e7eb;transition:border-color .15s}
|
|
177
178
|
.timeline-entry:hover{border-color:#d1d5db}
|
|
179
|
+
.timeline-entry--all-passed{border-left-color:#22c55e}
|
|
180
|
+
.timeline-entry--all-passed:hover{border-color:#86efac}
|
|
178
181
|
.timeline-entry--has-failures{border-left-color:#ef4444}
|
|
179
182
|
.timeline-entry--has-failures:hover{border-color:#fca5a5}
|
|
180
183
|
.timeline-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;
|
|
@@ -186,13 +189,22 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
186
189
|
text-overflow:ellipsis;white-space:nowrap;max-width:500px;min-width:0}
|
|
187
190
|
.nav-type-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:9999px;
|
|
188
191
|
white-space:nowrap;text-transform:uppercase;letter-spacing:.02em;flex-shrink:0}
|
|
189
|
-
.badge-goto
|
|
190
|
-
.badge-
|
|
191
|
-
.badge-
|
|
192
|
-
.badge-
|
|
192
|
+
.badge-goto{background:#dbeafe;color:#1e40af}
|
|
193
|
+
.badge-navigation{background:#e0e7ff;color:#3730a3}
|
|
194
|
+
.badge-page_load{background:#c7d2fe;color:#312e81}
|
|
195
|
+
.badge-back{background:#cffafe;color:#155e75}
|
|
196
|
+
.badge-forward{background:#a5f3fc;color:#164e63}
|
|
197
|
+
.badge-spa_route{background:#ede9fe;color:#5b21b6}
|
|
198
|
+
.badge-spa_replace{background:#ddd6fe;color:#4c1d95}
|
|
199
|
+
.badge-hash_change{background:#fae8ff;color:#86198f}
|
|
200
|
+
.badge-popstate{background:#f5d0fe;color:#701a75}
|
|
201
|
+
.badge-link_click{background:#fce7f3;color:#9d174d}
|
|
202
|
+
.badge-form_submit{background:#fbcfe8;color:#831843}
|
|
193
203
|
.badge-redirect{background:#ffedd5;color:#9a3412}
|
|
194
204
|
.badge-refresh{background:#e0f2fe;color:#075985}
|
|
195
|
-
.badge-dummy
|
|
205
|
+
.badge-dummy{background:#f3f4f6;color:#6b7280}
|
|
206
|
+
.badge-fallback{background:#e5e7eb;color:#4b5563}
|
|
207
|
+
.badge-manual_record{background:#fef9c3;color:#854d0e}
|
|
196
208
|
.timeline-info{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b7280;
|
|
197
209
|
margin-left:auto;flex-shrink:0;white-space:nowrap}
|
|
198
210
|
.spec-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
|
|
@@ -204,19 +216,38 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
204
216
|
.spec-summary{font-size:12px;color:#6b7280;margin-bottom:8px;display:flex;gap:12px;flex-wrap:wrap}
|
|
205
217
|
.spec-summary span{font-weight:600}
|
|
206
218
|
.spec-passed{color:#16a34a}.spec-failed{color:#dc2626}.spec-flaky{color:#d97706}
|
|
207
|
-
.test-card{
|
|
208
|
-
border-bottom:1px solid #f3f4f6}
|
|
219
|
+
.test-card{padding:8px 0;border-bottom:1px solid #f3f4f6}
|
|
209
220
|
.test-card:last-child{border-bottom:none}
|
|
221
|
+
.test-card-row{display:flex;align-items:flex-start;gap:8px}
|
|
210
222
|
.status-badge{font-size:11px;font-weight:700;padding:2px 8px;border-radius:9999px;
|
|
211
223
|
flex-shrink:0;margin-top:2px;text-transform:uppercase;letter-spacing:.03em}
|
|
212
224
|
.status-passed{background:#d1fae5;color:#065f46}
|
|
213
225
|
.status-failed{background:#fecaca;color:#991b1b}
|
|
214
226
|
.status-flaky{background:#fde68a;color:#92400e}
|
|
227
|
+
.status-skipped{background:#f3f4f6;color:#6b7280}
|
|
228
|
+
.status-timedout{background:#ffedd5;color:#9a3412}
|
|
229
|
+
.type-badge{background:#ede9fe;color:#5b21b6;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
230
|
+
.flaky-badge{background:#fef3c7;color:#92400e;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
231
|
+
.file-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:#6b7280}
|
|
232
|
+
.suite-name{font-size:11px;color:#9ca3af}
|
|
233
|
+
.status-mismatch{font-size:10px;color:#dc2626;font-weight:600}
|
|
234
|
+
.card-timedout{border-color:#ffedd5;background:#fff7ed}.card-timedout .count{color:#ea580c}
|
|
215
235
|
.test-info{flex:1;min-width:0}
|
|
216
236
|
.test-title{font-size:13px;color:#171717;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
217
237
|
.test-meta{display:flex;gap:8px;align-items:center;margin-top:2px;font-size:11px;color:#9ca3af}
|
|
218
238
|
.retry-badge{background:#fef3c7;color:#92400e;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
219
239
|
.tag-badge{background:#eff6ff;color:#1d4ed8;padding:1px 5px;border-radius:3px;font-size:10px}
|
|
240
|
+
.meta-prefix{font-size:10px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
|
241
|
+
.test-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
|
|
242
|
+
color:#6b7280;background:#f3f4f6;padding:1px 6px;border-radius:3px}
|
|
243
|
+
.network-panel--has-failures{border-color:#fecaca}
|
|
244
|
+
.network-panel--has-failures .network-header{background:#fef2f2;color:#991b1b}
|
|
245
|
+
.failed-requests-banner{display:flex;align-items:center;gap:6px;padding:8px 12px;
|
|
246
|
+
background:#fef2f2;color:#991b1b;font-size:12px;font-weight:600;border-top:1px solid #fecaca}
|
|
247
|
+
.failed-urls-list{padding:6px 12px;background:#fef2f2;border-top:1px solid #fecaca;font-size:11px;
|
|
248
|
+
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;max-height:200px;overflow-y:auto}
|
|
249
|
+
.failed-url-item{padding:2px 0;color:#991b1b;word-break:break-all}
|
|
250
|
+
.failed-url-status{font-weight:700;margin-right:4px}
|
|
220
251
|
.test-duration{font-size:12px;color:#9ca3af;flex-shrink:0;margin-top:2px}
|
|
221
252
|
/* Failure Details */
|
|
222
253
|
.failure-toggle{font-size:12px;color:#03b79c;cursor:pointer;margin-top:4px;
|
|
@@ -267,6 +298,34 @@ body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Rob
|
|
|
267
298
|
.section-title{font-size:16px;font-weight:700;color:#171717;margin-bottom:12px;
|
|
268
299
|
display:flex;align-items:center;gap:8px}
|
|
269
300
|
.section-title::before{content:'';width:4px;height:18px;background:#03b79c;border-radius:2px}
|
|
301
|
+
/* Artifacts Panel */
|
|
302
|
+
.artifacts-panel{margin-top:10px;padding:10px 12px;background:#f9fafb;border:1px solid #e5e7eb;
|
|
303
|
+
border-radius:8px}
|
|
304
|
+
.artifacts-panel-header{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
|
|
305
|
+
color:#6b7280;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
|
|
306
|
+
.artifacts-row{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap}
|
|
307
|
+
/* Screenshot */
|
|
308
|
+
.artifact-screenshot-wrap{display:flex;flex-direction:column;gap:4px;flex-shrink:0}
|
|
309
|
+
.artifact-thumbnail{height:140px;width:auto;border-radius:6px;cursor:pointer;
|
|
310
|
+
border:1px solid #e5e7eb;transition:border-color .15s,box-shadow .15s;object-fit:cover;display:block}
|
|
311
|
+
.artifact-thumbnail:hover{border-color:#03b79c;box-shadow:0 0 0 2px rgba(3,183,156,.2)}
|
|
312
|
+
.artifact-label{font-size:10px;color:#9ca3af;text-align:center}
|
|
313
|
+
/* Video */
|
|
314
|
+
.artifact-video-wrap{flex:1;min-width:200px;max-width:480px;display:flex;flex-direction:column;gap:4px}
|
|
315
|
+
.artifact-video-container{border-radius:6px;overflow:hidden;border:1px solid #e5e7eb;display:none}
|
|
316
|
+
.artifact-video-container.show{display:block}
|
|
317
|
+
.artifact-video{width:100%;max-height:320px;display:block;background:#000}
|
|
318
|
+
.video-toggle{font-size:12px;color:#03b79c;cursor:pointer;
|
|
319
|
+
border:none;background:none;padding:0;text-decoration:underline;font-family:inherit}
|
|
320
|
+
.video-toggle:hover{color:#028a75}
|
|
321
|
+
/* Lightbox */
|
|
322
|
+
.lightbox-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:1000;
|
|
323
|
+
display:flex;align-items:center;justify-content:center;cursor:zoom-out}
|
|
324
|
+
.lightbox-overlay img{max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.4)}
|
|
325
|
+
.lightbox-close{position:fixed;top:16px;right:16px;z-index:1001;width:36px;height:36px;
|
|
326
|
+
border-radius:50%;border:none;background:rgba(255,255,255,.15);color:#fff;font-size:20px;
|
|
327
|
+
cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s}
|
|
328
|
+
.lightbox-close:hover{background:rgba(255,255,255,.3)}
|
|
270
329
|
`;
|
|
271
330
|
var JS = `
|
|
272
331
|
(function(){
|
|
@@ -311,6 +370,7 @@ var JS = `
|
|
|
311
370
|
h+='<div class="summary-card card-failed"><div class="count">'+s.failed+'</div><div class="label">Failed</div></div>';
|
|
312
371
|
h+='<div class="summary-card card-flaky"><div class="count">'+s.flaky+'</div><div class="label">Flaky</div></div>';
|
|
313
372
|
h+='<div class="summary-card card-skipped"><div class="count">'+s.skipped+'</div><div class="label">Skipped</div></div>';
|
|
373
|
+
if(s.timedout!==undefined){h+='<div class="summary-card card-timedout"><div class="count">'+s.timedout+'</div><div class="label">Timed Out</div></div>';}
|
|
314
374
|
h+='</div>';
|
|
315
375
|
|
|
316
376
|
// Timeline
|
|
@@ -322,7 +382,8 @@ var JS = `
|
|
|
322
382
|
for(var i=0;i<data.timeline.length;i++){
|
|
323
383
|
var e=data.timeline[i];
|
|
324
384
|
var hasFail=e.tests.some(function(t){return t.status==='failed'});
|
|
325
|
-
var
|
|
385
|
+
var allPassed=!hasFail&&e.tests.length>0&&e.tests.every(function(t){return t.status==='passed'||t.status==='flaky'});
|
|
386
|
+
var cls='timeline-entry'+(hasFail?' timeline-entry--has-failures':'')+(allPassed?' timeline-entry--all-passed':'');
|
|
326
387
|
h+='<div class="'+cls+'" data-idx="'+i+'">';
|
|
327
388
|
h+='<div class="timeline-header" onclick="toggleEntry(this)">';
|
|
328
389
|
h+='<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
|
|
@@ -347,23 +408,35 @@ var JS = `
|
|
|
347
408
|
if(!tests||tests.length===0)return '';
|
|
348
409
|
var out='<div class="tests-section">';
|
|
349
410
|
// Spec summary
|
|
350
|
-
var pc=0,fc=0,fkc=0;
|
|
351
|
-
tests.forEach(function(t){if(t.status==='passed')pc++;else if(t.status==='failed')fc++;else if(t.status==='flaky')fkc++});
|
|
411
|
+
var pc=0,fc=0,fkc=0,sc=0,toc=0;
|
|
412
|
+
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++});
|
|
352
413
|
out+='<div class="spec-summary">';
|
|
353
414
|
if(pc>0)out+='<span class="spec-passed">'+pc+' passed</span>';
|
|
354
415
|
if(fc>0)out+='<span class="spec-failed">'+fc+' failed</span>';
|
|
355
416
|
if(fkc>0)out+='<span class="spec-flaky">'+fkc+' flaky</span>';
|
|
417
|
+
if(sc>0)out+='<span style="color:#6b7280;font-weight:600">'+sc+' skipped</span>';
|
|
418
|
+
if(toc>0)out+='<span style="color:#ea580c;font-weight:600">'+toc+' timed out</span>';
|
|
356
419
|
out+='</div>';
|
|
357
420
|
for(var j=0;j<tests.length;j++){
|
|
358
421
|
var t=tests[j];
|
|
359
422
|
out+='<div class="test-card">';
|
|
423
|
+
out+='<div class="test-card-row">';
|
|
360
424
|
out+='<span class="status-badge status-'+t.status+'">'+t.status+'</span>';
|
|
361
425
|
out+='<div class="test-info">';
|
|
362
426
|
out+='<div class="test-title" title="'+esc(t.title)+'">'+esc(t.title)+'</div>';
|
|
363
427
|
out+='<div class="test-meta">';
|
|
428
|
+
if(t.testType&&t.testType!=='unknown')out+='<span class="type-badge">'+esc(t.testType)+'</span>';
|
|
429
|
+
if(t.isFlaky)out+='<span class="flaky-badge">flaky</span>';
|
|
364
430
|
if(t.retryCount>0)out+='<span class="retry-badge">'+t.retryCount+' retries</span>';
|
|
365
|
-
if(t.
|
|
431
|
+
if(t.retryStatus)out+='<span class="retry-badge">'+esc(t.retryStatus)+'</span>';
|
|
432
|
+
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>'});}
|
|
433
|
+
if(t.expectedStatus&&t.actualStatus&&t.expectedStatus!==t.actualStatus)out+='<span class="status-mismatch">expected: '+esc(t.expectedStatus)+' actual: '+esc(t.actualStatus)+'</span>';
|
|
366
434
|
out+='</div>';
|
|
435
|
+
if(t.testId||t.filePath||t.suiteName){out+='<div class="test-meta">';
|
|
436
|
+
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>';
|
|
437
|
+
if(t.filePath)out+='<span class="meta-prefix">File:</span><span class="file-path">'+esc(t.filePath)+'</span>';
|
|
438
|
+
if(t.suiteName)out+='<span class="meta-prefix">Suite:</span><span class="suite-name">'+esc(t.suiteName)+'</span>';
|
|
439
|
+
out+='</div>';}
|
|
367
440
|
if((t.status==='failed'||t.status==='flaky')&&t.failure){
|
|
368
441
|
var fid='fail-'+Math.random().toString(36).substr(2,9);
|
|
369
442
|
out+='<button class="failure-toggle" onclick="toggleFail(\\''+fid+'\\')">Show details</button>';
|
|
@@ -387,6 +460,26 @@ var JS = `
|
|
|
387
460
|
out+='</div>';
|
|
388
461
|
out+='<span class="test-duration">'+fmtDur(t.duration)+'</span>';
|
|
389
462
|
out+='</div>';
|
|
463
|
+
if(t.artifacts&&(t.artifacts.screenshot||t.artifacts.video)){
|
|
464
|
+
out+='<div class="artifacts-panel">';
|
|
465
|
+
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>';
|
|
466
|
+
out+='<div class="artifacts-row">';
|
|
467
|
+
if(t.artifacts.screenshot){
|
|
468
|
+
out+='<div class="artifact-screenshot-wrap">';
|
|
469
|
+
out+='<img class="artifact-thumbnail" src="'+esc(t.artifacts.screenshot)+'" loading="lazy" alt="Screenshot" onclick="openLightbox(this.src)">';
|
|
470
|
+
out+='<span class="artifact-label">Click to enlarge</span>';
|
|
471
|
+
out+='</div>';
|
|
472
|
+
}
|
|
473
|
+
if(t.artifacts.video){
|
|
474
|
+
var vid='video-'+Math.random().toString(36).substr(2,9);
|
|
475
|
+
out+='<div class="artifact-video-wrap">';
|
|
476
|
+
out+='<button class="video-toggle" onclick="toggleVideo(\\''+vid+'\\')">Show video</button>';
|
|
477
|
+
out+='<div class="artifact-video-container" id="'+vid+'"><video class="artifact-video" controls preload="none" src="'+esc(t.artifacts.video)+'"></video></div>';
|
|
478
|
+
out+='</div>';
|
|
479
|
+
}
|
|
480
|
+
out+='</div></div>';
|
|
481
|
+
}
|
|
482
|
+
out+='</div>';
|
|
390
483
|
}
|
|
391
484
|
out+='</div>';
|
|
392
485
|
return out;
|
|
@@ -412,13 +505,17 @@ var JS = `
|
|
|
412
505
|
|
|
413
506
|
function renderNetwork(stats){
|
|
414
507
|
if(!stats)return '<div class="no-network">No network data captured</div>';
|
|
415
|
-
var
|
|
508
|
+
var hasFailedReqs=stats.failedRequests>0;
|
|
509
|
+
var out='<div class="network-panel'+(hasFailedReqs?' network-panel--has-failures':'')+'">';
|
|
416
510
|
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>';
|
|
417
511
|
out+='<div class="network-summary">';
|
|
418
512
|
out+='<div class="net-stat"><span class="val">'+stats.totalRequests+'</span><span class="lbl">Requests</span></div>';
|
|
419
|
-
out+='<div class="net-stat'+(
|
|
513
|
+
out+='<div class="net-stat'+(hasFailedReqs?' has-failed':'')+'"><span class="val">'+stats.failedRequests+'</span><span class="lbl">Failed</span></div>';
|
|
420
514
|
out+='<div class="net-stat"><span class="val">'+fmtBytes(stats.totalBytes)+'</span><span class="lbl">Transferred</span></div>';
|
|
421
515
|
out+='</div>';
|
|
516
|
+
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>';
|
|
517
|
+
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>';}
|
|
518
|
+
}
|
|
422
519
|
if(stats.byType){
|
|
423
520
|
var types=[['XHR','xhr'],['Document','document'],['Script','script'],['Stylesheet','stylesheet'],['Image','image'],['Font','font'],['Other','other']];
|
|
424
521
|
out+='<div class="resource-breakdown">';
|
|
@@ -437,6 +534,21 @@ var JS = `
|
|
|
437
534
|
function toggleEntry(el){el.closest('.timeline-entry').classList.toggle('expanded')}
|
|
438
535
|
function toggleFail(id){document.getElementById(id).classList.toggle('show')}
|
|
439
536
|
function toggleStack(id){document.getElementById(id).classList.toggle('show')}
|
|
537
|
+
function openLightbox(src){var o=document.createElement('div');o.className='lightbox-overlay';
|
|
538
|
+
o.onclick=function(){closeLightbox()};o.innerHTML='<img src="'+src.replace(/"/g,'"')+'" alt="Screenshot">';
|
|
539
|
+
var b=document.createElement('button');b.className='lightbox-close';b.innerHTML='×';
|
|
540
|
+
b.onclick=function(e){e.stopPropagation();closeLightbox()};
|
|
541
|
+
document.body.appendChild(o);document.body.appendChild(b);
|
|
542
|
+
document.addEventListener('keydown',lightboxEsc)}
|
|
543
|
+
function closeLightbox(){var o=document.querySelector('.lightbox-overlay');if(o)o.remove();
|
|
544
|
+
var b=document.querySelector('.lightbox-close');if(b)b.remove();
|
|
545
|
+
document.removeEventListener('keydown',lightboxEsc)}
|
|
546
|
+
function lightboxEsc(e){if(e.key==='Escape')closeLightbox()}
|
|
547
|
+
function toggleVideo(id){var el=document.getElementById(id);if(!el)return;
|
|
548
|
+
var isShown=el.classList.toggle('show');
|
|
549
|
+
var btn=el.previousElementSibling;
|
|
550
|
+
if(btn)btn.textContent=isShown?'Hide video':'Show video';
|
|
551
|
+
if(!isShown){var v=el.querySelector('video');if(v)v.pause()}}
|
|
440
552
|
`;
|
|
441
553
|
function renderHtmlDocument(reportJson) {
|
|
442
554
|
const jsWithLogo = JS.replace(/__LOGO_SVG__/g, LOGO_SVG.replace(/'/g, "\\'").replace(/\n/g, ""));
|
|
@@ -446,7 +558,7 @@ function renderHtmlDocument(reportJson) {
|
|
|
446
558
|
<head>
|
|
447
559
|
<meta charset="UTF-8">
|
|
448
560
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
449
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data
|
|
561
|
+
<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';">
|
|
450
562
|
<title>TestRelic Report</title>
|
|
451
563
|
<style>${CSS}</style>
|
|
452
564
|
</head>
|
|
@@ -509,7 +621,108 @@ function writeHtmlReport(report, config) {
|
|
|
509
621
|
}
|
|
510
622
|
}
|
|
511
623
|
|
|
624
|
+
// src/artifact-manager.ts
|
|
625
|
+
import { mkdirSync as mkdirSync2, copyFileSync, existsSync } from "fs";
|
|
626
|
+
import { join, extname } from "path";
|
|
627
|
+
function sanitizeFolderName(title) {
|
|
628
|
+
let name = title.replace(/[^a-zA-Z0-9\-_ ]/g, "-").replace(/\s+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
629
|
+
if (name.length > 100) {
|
|
630
|
+
name = name.substring(0, 100).replace(/-+$/, "");
|
|
631
|
+
}
|
|
632
|
+
return name || "unnamed-test";
|
|
633
|
+
}
|
|
634
|
+
function copyArtifacts(attachments, testTitle, retryCount, outputDir) {
|
|
635
|
+
const screenshot = attachments.find(
|
|
636
|
+
(a) => a.name === "screenshot" && a.path
|
|
637
|
+
);
|
|
638
|
+
const video = attachments.find(
|
|
639
|
+
(a) => a.name === "video" && a.path
|
|
640
|
+
);
|
|
641
|
+
if (!screenshot && !video) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
let folderName = sanitizeFolderName(testTitle);
|
|
645
|
+
if (retryCount > 0) {
|
|
646
|
+
folderName += `--retry-${retryCount}`;
|
|
647
|
+
}
|
|
648
|
+
const artifactDir = join(outputDir, "artifacts", folderName);
|
|
649
|
+
const result = {};
|
|
650
|
+
try {
|
|
651
|
+
mkdirSync2(artifactDir, { recursive: true });
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
if (screenshot?.path) {
|
|
656
|
+
try {
|
|
657
|
+
if (existsSync(screenshot.path)) {
|
|
658
|
+
const ext = extname(screenshot.path) || ".png";
|
|
659
|
+
const destName = `screenshot${ext}`;
|
|
660
|
+
copyFileSync(screenshot.path, join(artifactDir, destName));
|
|
661
|
+
result.screenshot = `artifacts/${folderName}/${destName}`;
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (video?.path) {
|
|
667
|
+
try {
|
|
668
|
+
if (existsSync(video.path)) {
|
|
669
|
+
const ext = extname(video.path) || ".webm";
|
|
670
|
+
const destName = `video${ext}`;
|
|
671
|
+
copyFileSync(video.path, join(artifactDir, destName));
|
|
672
|
+
result.video = `artifacts/${folderName}/${destName}`;
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!result.screenshot && !result.video) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
|
|
512
683
|
// src/reporter.ts
|
|
684
|
+
function mapPlaywrightStatus(status) {
|
|
685
|
+
switch (status) {
|
|
686
|
+
case "passed":
|
|
687
|
+
return "passed";
|
|
688
|
+
case "failed":
|
|
689
|
+
return "failed";
|
|
690
|
+
case "timedOut":
|
|
691
|
+
return "timedout";
|
|
692
|
+
case "skipped":
|
|
693
|
+
return "skipped";
|
|
694
|
+
case "interrupted":
|
|
695
|
+
return "failed";
|
|
696
|
+
default:
|
|
697
|
+
return "failed";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function generateTestId(filePath, suiteName, title) {
|
|
701
|
+
const input = `${filePath}::${suiteName}::${title}`;
|
|
702
|
+
return createHash("sha256").update(input).digest("hex").substring(0, 16);
|
|
703
|
+
}
|
|
704
|
+
function getSuiteName(titlePath) {
|
|
705
|
+
if (titlePath.length <= 4) return "";
|
|
706
|
+
return titlePath[titlePath.length - 2];
|
|
707
|
+
}
|
|
708
|
+
function getRetryStatus(results) {
|
|
709
|
+
const passedIndex = results.findIndex((r) => r.status === "passed");
|
|
710
|
+
return passedIndex > 0 ? `passed on retry ${passedIndex}` : null;
|
|
711
|
+
}
|
|
712
|
+
function getTestType(tags, filePath) {
|
|
713
|
+
const typeOrder = ["e2e", "api", "unit"];
|
|
714
|
+
for (const type of typeOrder) {
|
|
715
|
+
if (tags.some((tag) => tag === `@${type}` || tag === type)) {
|
|
716
|
+
return type;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const type of typeOrder) {
|
|
720
|
+
if (filePath.includes(`/${type}/`)) {
|
|
721
|
+
return type;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return "unknown";
|
|
725
|
+
}
|
|
513
726
|
var TestRelicReporter = class {
|
|
514
727
|
constructor(options) {
|
|
515
728
|
this.rootDir = "";
|
|
@@ -533,12 +746,11 @@ var TestRelicReporter = class {
|
|
|
533
746
|
let status;
|
|
534
747
|
if (outcome === "flaky") {
|
|
535
748
|
status = "flaky";
|
|
536
|
-
} else if (
|
|
537
|
-
status = "
|
|
749
|
+
} else if (outcome === "skipped") {
|
|
750
|
+
status = "skipped";
|
|
538
751
|
} else {
|
|
539
|
-
status =
|
|
752
|
+
status = mapPlaywrightStatus(lastResult.status);
|
|
540
753
|
}
|
|
541
|
-
if (outcome === "skipped") return;
|
|
542
754
|
const startedAt = lastResult.startTime.toISOString();
|
|
543
755
|
const completedAt = new Date(lastResult.startTime.getTime() + lastResult.duration).toISOString();
|
|
544
756
|
const tags = test.tags ? [...test.tags] : test.annotations.filter((a) => a.type === "tag").map((a) => a.description ?? "");
|
|
@@ -575,9 +787,24 @@ var TestRelicReporter = class {
|
|
|
575
787
|
}
|
|
576
788
|
const titlePath = test.titlePath().filter(Boolean);
|
|
577
789
|
const specFile = relative(this.rootDir || ".", test.location.file);
|
|
790
|
+
const suiteName = getSuiteName(titlePath);
|
|
791
|
+
const title = titlePath.join(" > ");
|
|
792
|
+
const filePath = specFile;
|
|
793
|
+
const testId = generateTestId(filePath, suiteName, title);
|
|
794
|
+
const testType = getTestType(tags, filePath);
|
|
795
|
+
const isFlaky = outcome === "flaky";
|
|
796
|
+
const retryStatus = getRetryStatus(test.results);
|
|
797
|
+
const expectedStatus = mapPlaywrightStatus(test.expectedStatus);
|
|
798
|
+
const actualStatus = mapPlaywrightStatus(lastResult.status);
|
|
799
|
+
let artifacts = null;
|
|
800
|
+
if (this.config.includeArtifacts && status !== "skipped" && lastResult.attachments) {
|
|
801
|
+
const outputDir = dirname2(this.config.outputPath);
|
|
802
|
+
const lastTitle = titlePath[titlePath.length - 1] ?? test.title;
|
|
803
|
+
artifacts = copyArtifacts(lastResult.attachments, lastTitle, lastResult.retry, outputDir);
|
|
804
|
+
}
|
|
578
805
|
this.collectedTests.push({
|
|
579
806
|
titlePath,
|
|
580
|
-
title
|
|
807
|
+
title,
|
|
581
808
|
status,
|
|
582
809
|
duration: lastResult.duration,
|
|
583
810
|
startedAt,
|
|
@@ -586,7 +813,16 @@ var TestRelicReporter = class {
|
|
|
586
813
|
tags,
|
|
587
814
|
failure,
|
|
588
815
|
specFile,
|
|
589
|
-
navigations
|
|
816
|
+
navigations,
|
|
817
|
+
testId,
|
|
818
|
+
filePath,
|
|
819
|
+
suiteName,
|
|
820
|
+
testType,
|
|
821
|
+
isFlaky,
|
|
822
|
+
retryStatus,
|
|
823
|
+
expectedStatus,
|
|
824
|
+
actualStatus,
|
|
825
|
+
artifacts
|
|
590
826
|
});
|
|
591
827
|
} catch {
|
|
592
828
|
}
|
|
@@ -666,6 +902,7 @@ var TestRelicReporter = class {
|
|
|
666
902
|
let failed = 0;
|
|
667
903
|
let flaky = 0;
|
|
668
904
|
let skipped = 0;
|
|
905
|
+
let timedout = 0;
|
|
669
906
|
for (const test of this.collectedTests) {
|
|
670
907
|
switch (test.status) {
|
|
671
908
|
case "passed":
|
|
@@ -677,6 +914,12 @@ var TestRelicReporter = class {
|
|
|
677
914
|
case "flaky":
|
|
678
915
|
flaky++;
|
|
679
916
|
break;
|
|
917
|
+
case "skipped":
|
|
918
|
+
skipped++;
|
|
919
|
+
break;
|
|
920
|
+
case "timedout":
|
|
921
|
+
timedout++;
|
|
922
|
+
break;
|
|
680
923
|
}
|
|
681
924
|
}
|
|
682
925
|
return {
|
|
@@ -684,7 +927,8 @@ var TestRelicReporter = class {
|
|
|
684
927
|
passed,
|
|
685
928
|
failed,
|
|
686
929
|
flaky,
|
|
687
|
-
skipped
|
|
930
|
+
skipped,
|
|
931
|
+
timedout
|
|
688
932
|
};
|
|
689
933
|
}
|
|
690
934
|
toTestResult(test) {
|
|
@@ -696,7 +940,16 @@ var TestRelicReporter = class {
|
|
|
696
940
|
completedAt: test.completedAt,
|
|
697
941
|
retryCount: test.retryCount,
|
|
698
942
|
tags: test.tags,
|
|
699
|
-
failure: test.failure
|
|
943
|
+
failure: test.failure,
|
|
944
|
+
testId: test.testId,
|
|
945
|
+
filePath: test.filePath,
|
|
946
|
+
suiteName: test.suiteName,
|
|
947
|
+
testType: test.testType,
|
|
948
|
+
isFlaky: test.isFlaky,
|
|
949
|
+
retryStatus: test.retryStatus,
|
|
950
|
+
expectedStatus: test.expectedStatus,
|
|
951
|
+
actualStatus: test.actualStatus,
|
|
952
|
+
artifacts: test.artifacts
|
|
700
953
|
};
|
|
701
954
|
}
|
|
702
955
|
writeReport(report) {
|
|
@@ -704,7 +957,7 @@ var TestRelicReporter = class {
|
|
|
704
957
|
const json = JSON.stringify(report, null, 2);
|
|
705
958
|
const outputPath = this.config.outputPath;
|
|
706
959
|
const dir = dirname2(outputPath);
|
|
707
|
-
|
|
960
|
+
mkdirSync3(dir, { recursive: true });
|
|
708
961
|
const tmpPath = outputPath + ".tmp";
|
|
709
962
|
writeFileSync2(tmpPath, json, "utf-8");
|
|
710
963
|
renameSync2(tmpPath, outputPath);
|