@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/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 import_node_fs3 = require("fs");
32
- var import_node_path2 = require("path");
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.0.0";
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,.badge-navigation,.badge-page_load{background:#d1fae5;color:#065f46}
218
- .badge-back,.badge-forward{background:#dbeafe;color:#1e40af}
219
- .badge-spa_route,.badge-spa_replace,.badge-hash_change,.badge-popstate{background:#ede9fe;color:#5b21b6}
220
- .badge-link_click,.badge-form_submit{background:#fce7f3;color:#9d174d}
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,.badge-fallback,.badge-manual_record{background:#f3f4f6;color:#6b7280}
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{display:flex;align-items:flex-start;gap:8px;padding:8px 0;
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 cls='timeline-entry'+(hasFail?' timeline-entry--has-failures':'');
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.tags)t.tags.forEach(function(tag){if(tag)out+='<span class="tag-badge">'+esc(tag)+'</span>'});
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 out='<div class="network-panel">';
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'+(stats.failedRequests>0?' has-failed':'')+'"><span class="val">'+stats.failedRequests+'</span><span class="lbl">Failed</span></div>';
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,'&quot;')+'" alt="Screenshot">';
567
+ var b=document.createElement('button');b.className='lightbox-close';b.innerHTML='&times;';
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 (lastResult.status === "passed") {
565
- status = "passed";
777
+ } else if (outcome === "skipped") {
778
+ status = "skipped";
566
779
  } else {
567
- status = "failed";
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, import_node_path2.relative)(this.rootDir || ".", test.location.file);
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: titlePath.join(" > "),
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, import_node_path2.dirname)(outputPath);
735
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
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, import_node_fs3.writeFileSync)(tmpPath, json, "utf-8");
738
- (0, import_node_fs3.renameSync)(tmpPath, outputPath);
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)}