@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/reporter.ts
2
- import { randomUUID } from "crypto";
3
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "fs";
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.0.0";
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,.badge-navigation,.badge-page_load{background:#d1fae5;color:#065f46}
190
- .badge-back,.badge-forward{background:#dbeafe;color:#1e40af}
191
- .badge-spa_route,.badge-spa_replace,.badge-hash_change,.badge-popstate{background:#ede9fe;color:#5b21b6}
192
- .badge-link_click,.badge-form_submit{background:#fce7f3;color:#9d174d}
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,.badge-fallback,.badge-manual_record{background:#f3f4f6;color:#6b7280}
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{display:flex;align-items:flex-start;gap:8px;padding:8px 0;
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 cls='timeline-entry'+(hasFail?' timeline-entry--has-failures':'');
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.tags)t.tags.forEach(function(tag){if(tag)out+='<span class="tag-badge">'+esc(tag)+'</span>'});
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 out='<div class="network-panel">';
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'+(stats.failedRequests>0?' has-failed':'')+'"><span class="val">'+stats.failedRequests+'</span><span class="lbl">Failed</span></div>';
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,'&quot;')+'" alt="Screenshot">';
539
+ var b=document.createElement('button');b.className='lightbox-close';b.innerHTML='&times;';
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 (lastResult.status === "passed") {
537
- status = "passed";
749
+ } else if (outcome === "skipped") {
750
+ status = "skipped";
538
751
  } else {
539
- status = "failed";
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: titlePath.join(" > "),
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
- mkdirSync2(dir, { recursive: true });
960
+ mkdirSync3(dir, { recursive: true });
708
961
  const tmpPath = outputPath + ".tmp";
709
962
  writeFileSync2(tmpPath, json, "utf-8");
710
963
  renameSync2(tmpPath, outputPath);