reporting-labs 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -160,6 +160,7 @@ Run it two or three times to see the history features (new vs known failures, fl
160
160
 
161
161
  ### Failures: everything you need to triage
162
162
 
163
+ - **Bug report** button on every failed test: a ready-to-paste ticket with priority, owner, story, environment, test data, steps to reproduce (taken from the test's own steps, the failing one marked), expected vs actual, the error, API calls and attachment names. Pick Markdown, Jira or plain text, edit if you like, copy or download.
163
164
  - **By owner**: who to ping, with failed and flaky counts. Click an owner to filter.
164
165
  - **Download CSV / JSON**: all failed and flaky tests with title, spec, project, priority, owner, ticket, attempts, duration, first error line and new/known status. Ready for Jira or a sheet.
165
166
  - **Copy summary**: a Slack or Teams message with the top failures, owners and ticket keys.
package/dist/template.js CHANGED
@@ -176,7 +176,7 @@ a{color:var(--accent)}
176
176
  .donut svg circle{stroke-linecap:butt}
177
177
  /* failure clusters */
178
178
  .clu{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
179
- .clu li{border:1px solid var(--line);border-left:3px solid var(--fail);border-radius:var(--radius);padding:8px 10px}
179
+ .clu li{border:1px solid var(--line);border-radius:var(--radius);padding:8px 10px}
180
180
  .clu .msg{font:12px/1.45 var(--mono);white-space:pre-wrap;word-break:break-word;color:var(--ink);max-height:3em;overflow:hidden}
181
181
  .clu .who{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
182
182
  .clu .who button{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--fail-bg);color:var(--fail)} .clu .who button:hover{outline:1px solid var(--fail)}
@@ -283,7 +283,7 @@ a{color:var(--accent)}
283
283
  .tab{padding:8px 12px;font-size:13px;color:var(--ink-2);border-bottom:2px solid transparent;margin-bottom:-1px}
284
284
  .tab[aria-selected=true]{color:var(--ink);border-bottom-color:var(--accent)}
285
285
  .detail h4{font-size:13px;font-weight:600;color:var(--ink-2);margin:20px 0 8px}
286
- .err{background:var(--fail-bg);border-left:3px solid var(--fail);padding:12px 14px;border-radius:0 var(--radius) var(--radius) 0;font:12.5px/1.55 var(--mono);white-space:pre-wrap;word-break:break-word;color:var(--ink)}
286
+ .err{background:var(--fail-bg);border:1px solid var(--line);padding:12px 14px;border-radius:var(--radius);font:12.5px/1.55 var(--mono);white-space:pre-wrap;word-break:break-word;color:var(--ink)}
287
287
  .err+.err{margin-top:10px}
288
288
  .err details{margin-top:8px} .err summary{cursor:pointer;color:var(--ink-2);font-family:var(--sans);font-size:12px}
289
289
  .err .stack{color:var(--ink-2);margin-top:6px}
@@ -306,6 +306,13 @@ li.collapsed>ul{display:none}
306
306
  .att .file a{font:12px var(--mono)}
307
307
  .txt{background:var(--surface-2);padding:10px 12px;border-radius:var(--radius);font:12px/1.5 var(--mono);white-space:pre-wrap;max-height:260px;overflow:auto}
308
308
  .lb{position:fixed;inset:0;background:rgba(0,0,0,.85);display:grid;place-items:center;z-index:10;cursor:zoom-out}
309
+ .bugdlg{position:fixed;inset:0;background:rgba(10,20,40,.55);display:grid;place-items:center;z-index:11;padding:20px}
310
+ .bug-box{background:var(--surface);color:var(--ink);border-radius:12px;width:min(860px,100%);max-height:92vh;display:flex;flex-direction:column;box-shadow:0 30px 80px -20px rgba(0,0,0,.5);border:1px solid var(--line)}
311
+ .bug-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--line)} .bug-head h3{margin:0;font-size:16px} .bug-head .hint{flex:1;color:var(--ink-3);font-size:12px}
312
+ .bug-tools{display:flex;align-items:center;gap:8px;padding:10px 18px;border-bottom:1px solid var(--line)} .bug-tools .grow{flex:1}
313
+ .bug-ta{flex:1;min-height:360px;margin:0;padding:14px 18px;border:0;resize:none;background:var(--surface-2);color:var(--ink);font:12.5px/1.55 var(--mono);outline:none;border-radius:0 0 12px 12px;white-space:pre}
314
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:#fff} .btn.primary.done{background:var(--pass);border-color:var(--pass)}
315
+ .detail .actions .btn{cursor:pointer}
309
316
  .lb img{max-width:95vw;max-height:95vh;border-radius:4px}
310
317
  .kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:13px}
311
318
  .kv dt{color:var(--ink-3)} .kv dd{margin:0}
@@ -408,8 +415,8 @@ li.collapsed>ul{display:none}
408
415
  .wk .n{font:11px var(--mono);color:var(--ink-3);text-align:right}
409
416
  .wk-sum{font-size:13px;color:var(--ink-2);margin-bottom:10px}
410
417
  /* list: group toggle + folder tree */
411
- .tools .seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
412
- .tools .seg button{padding:6px 10px;font-size:12px;color:var(--ink-2)} .tools .seg button[aria-pressed=true]{background:var(--surface-2);color:var(--ink)}
418
+ .tools .seg,.bug-tools .seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
419
+ .tools .seg button,.bug-tools .seg button{padding:6px 10px;font-size:12px;color:var(--ink-2);cursor:pointer} .tools .seg button[aria-pressed=true],.bug-tools .seg button[aria-pressed=true]{background:var(--surface-2);color:var(--ink)}
413
420
  .folder{display:flex;align-items:center;gap:6px;padding:8px 12px 4px;font:12px var(--mono);color:var(--ink-2);width:100%;text-align:left}
414
421
  .folder .cnt{margin-left:auto;display:flex;gap:6px} .folder .cnt b{color:var(--fail)} .folder .cnt span{color:var(--ink-3)}
415
422
  .folder .tw{font-size:9px;color:var(--ink-3)}
@@ -566,7 +573,7 @@ html,body{-webkit-font-smoothing:antialiased}
566
573
  .tools{padding:12px;gap:8px;background:var(--surface)}
567
574
  .tools input,.tools select{border-radius:8px;background:var(--bg);border-color:var(--line)}
568
575
  .tools input:focus,.tools select:focus{border-color:var(--accent);outline:0;box-shadow:0 0 0 3px var(--accent-tint)}
569
- .tools .seg{border-radius:8px} .tools .seg button[aria-pressed=true]{background:var(--accent-tint);color:var(--accent-2);font-weight:500}
576
+ .tools .seg,.bug-tools .seg{border-radius:8px} .tools .seg button[aria-pressed=true],.bug-tools .seg button[aria-pressed=true]{background:var(--accent-tint);color:var(--accent-2);font-weight:500}
570
577
  .item{border-left-width:3px;padding:9px 12px}
571
578
  .item[aria-current=true]{background:var(--accent-tint);border-left-color:var(--accent)}
572
579
  .item .tt .n{font-weight:500}
@@ -579,7 +586,7 @@ html,body{-webkit-font-smoothing:antialiased}
579
586
  .tabs{gap:6px} .tab{border-radius:8px 8px 0 0;font-weight:500}
580
587
  .tab[aria-selected=true]{color:var(--accent-2);border-bottom-color:var(--accent)}
581
588
  .detail h4{color:var(--accent-2);text-transform:uppercase;letter-spacing:.06em;font-size:11.5px}
582
- .err{border-radius:0 8px 8px 0}
589
+ .err{border-radius:8px}
583
590
  .meta{border-radius:8px;background:var(--surface)}
584
591
  .meta .k{text-transform:uppercase;font-size:10.5px;letter-spacing:.05em}
585
592
  .step{border-radius:6px} .step .cat{border-radius:4px}
@@ -620,12 +627,12 @@ html,body{-webkit-font-smoothing:antialiased}
620
627
  .note{padding:10px 14px;border-radius:8px;margin-bottom:10px;border:1px solid var(--line)} .note.ok{background:var(--pass-bg);color:var(--ink)} .note.warn{background:var(--flaky-bg);color:var(--flaky-ink)}
621
628
  .badge.xfail{background:var(--flaky-bg);color:var(--flaky-ink)}
622
629
  .errwrap+.errwrap{margin-top:14px}
623
- .why{border:1px solid var(--line);border-left:3px solid var(--fail);border-radius:0 var(--radius) var(--radius) 0;padding:10px 14px;margin-bottom:10px;background:var(--surface)}
630
+ .why{border:1px solid var(--line);border-radius:var(--radius);padding:10px 14px;margin-bottom:10px;background:var(--surface)}
624
631
  .why-c{font-size:12.5px;color:var(--ink);margin-bottom:2px;line-height:1.4} .why-c .why-k{margin-right:2px}
625
632
  .why-l{display:flex;align-items:center;gap:8px;margin-bottom:4px} .why-k{font-size:12px;font-weight:600;color:var(--fail)} .why-a{font:11.5px var(--mono);color:var(--ink-3)}
626
633
  .why-s{font-size:var(--fs);color:var(--ink);line-height:1.45} .why-h{font-size:12.5px;color:var(--ink-2);margin-top:4px;line-height:1.45}
627
- .why.assertion,.why.visual{border-left-color:#C77D14} .why.assertion .why-k,.why.visual .why-k{color:#9A6A00}
628
- .why.script,.why.thrown,.why.file{border-left-color:var(--ink-3)} .why.script .why-k,.why.thrown .why-k,.why.file .why-k{color:var(--ink-2)}
634
+ .why.assertion .why-k,.why.visual .why-k{color:#9A6A00}
635
+ .why.script .why-k,.why.thrown .why-k,.why.file .why-k{color:var(--ink-2)}
629
636
  .errloc{font-family:var(--mono);color:var(--ink-3);margin-bottom:6px} .errloc a{color:var(--accent-2);text-decoration:none} .errloc a:hover{text-decoration:underline}
630
637
  .snip{margin:10px 0 0;background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:10px 12px;font-family:var(--mono);line-height:1.55;white-space:pre;overflow:auto;color:var(--ink-2)}
631
638
  /* ---------- triage extras: history, diff, export, env, heatmap ---------- */
@@ -1325,7 +1332,7 @@ function renderDetail(t){
1325
1332
  d.append(h('div',{class:'crumb'}, [t.file, ...t.path.map(p=>data.bdd&&!/^Feature:/i.test(p)?'Feature: '+p:p)].map(p=>h('span',{},p))),
1326
1333
  h('div',{class:'titlebar'}, h('h3',{},t.title), h('div',{class:'actions'},
1327
1334
  h('button',{class:'btn',onclick:e=>copyText(location.href.split('#')[0]+'#t='+t.id,e.currentTarget,'Link copied')},'Copy link'),
1328
- (()=>{ const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; return e? h('button',{class:'btn',onclick:ev=>copyText(t.title+'\n'+t.file+':'+t.line+'\n\n'+e.message,ev.currentTarget,'Error copied')},'Copy error') : null; })(),
1335
+ (()=>{ const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; return e? [h('button',{class:'btn',onclick:ev=>copyText(t.title+'\n'+t.file+':'+t.line+'\n\n'+e.message,ev.currentTarget,'Error copied')},'Copy error'), h('button',{class:'btn primary',onclick:()=>openBugReport(t)},'Bug report')] : null; })(),
1329
1336
  editorLink(t))),
1330
1337
  h('div',{class:'badges'}, h('span',{class:'badge '+t.outcome},label[t.outcome]), t.expectedFailure? h('span',{class:'badge xfail',title:'Marked test.fail(): failing is the expected result'},'Expected failure') : null, sinceBadge(t), dots(t,10), data.bdd?h('span',{class:'badge scenario'},'Scenario'):null, t.tags.map(g=>h('span',{class:'badge tag'},g)), data.projects.length>1?h('span',{class:'badge tag'},t.project):null, h('span',{class:'loc'}, t.file+':'+t.line+' · '+ms(t.duration)+(t.outcome==='timedOut'&&t.timeout?' · timeout '+ms(t.timeout):'')+(t.retries?' · retries '+t.retries:''))));
1331
1338
  const metaKeys=Object.keys(t.meta);
@@ -1429,6 +1436,69 @@ function compare(set){
1429
1436
  for(const [v,l] of [['slider','Slider'],['side','Side by side'],['diff','Diff']]) tabs.append(h('button',{class:'tab','data-v':v,onclick:()=>{cur=v;render();}},l));
1430
1437
  wrap.append(tabs,stage); render(); return wrap;
1431
1438
  }
1439
+ /* ---- bug report: a ready-to-paste ticket built from the failure ---- */
1440
+ function bugModel(t){
1441
+ const r=t.results[t.results.length-1], e=r&&r.errors[0], x=e&&e.explain, msg=e?e.message:'';
1442
+ const ex=msg.match(/^(Expected[^:\n]{0,40}):[ \t]*(.+)$/m), rc=msg.match(/^(Received[^:\n]{0,40}):[ \t]*(.+)$/m);
1443
+ const si=sinceInfo(t);
1444
+ const steps=[]; const walk=(list,depth)=>{ for(const st of list){ if(st.category==='hook'||/^Attach "/.test(st.title)||/^Worker Cleanup/.test(st.title)) continue; const user=st.category==='test.step'; if(user||depth===0) steps.push({title:st.title.replace(/^(Given|When|Then|And|But)\s+/,m=>m), failed:!!st.error, user}); if(user&&st.steps.length&&!st.steps.some(c=>c.category==='test.step')) continue; if(st.steps.length) walk(st.steps,depth+1); } };
1445
+ if(r) walk(r.steps,0);
1446
+ const userSteps=steps.filter(s=>s.user); const repro=(userSteps.length?userSteps:steps).filter(s=>!/^Expect "/.test(s.title)||s.failed);
1447
+ const env=[]; if(data.options.project&&data.options.project.name) env.push(['Application', data.options.project.name+(data.options.project.version?' v'+data.options.project.version:'')]);
1448
+ for(const [k,v] of Object.entries(data.metadata||{})) env.push([k,v]);
1449
+ if(data.projects.length>1||t.project) env.push(['Playwright project', t.project]);
1450
+ for(const row of (data.env||[])) if(!/^(Workers|Shard)$/.test(row.k)) env.push([row.k, row.href&&row.href!==row.v? row.v+' ('+row.href+')' : row.v]);
1451
+ const dataBlocks=(r?r.data:[]).map(b=>{ if(b.kind==='kv') return {name:b.name, lines:b.kv.map(([k,v])=>k+': '+v)}; if(b.kind==='table') return {name:b.name, lines:b.rows.slice(0,5).map(row=>row.map((c,i)=>(b.columns[i]||'')+'='+c).join(', ')).concat(b.rows.length>5?['… '+(b.rows.length-5)+' more rows']:[])}; return {name:b.name, lines:[String(b.text||'').slice(0,300)]}; });
1452
+ const api=(r?r.api:[]).map(c=>c.method.toUpperCase()+' '+c.url+' → '+(c.status||'no response')+(c.duration!=null?' ('+ms(c.duration)+')':''));
1453
+ const logs=(r?r.logs:[]).slice(-8).map(l=>l.msg);
1454
+ const att=(r?r.attachments:[]).filter(a=>a.contentType.startsWith('image/')||a.contentType.startsWith('video/')||/zip/.test(a.contentType)).map(a=>{ const kind=a.contentType.startsWith('video/')?'video':/zip/.test(a.contentType)?'trace':'screenshot'; return a.name.toLowerCase()===kind? kind : a.name+' ('+kind+')'; });
1455
+ const expected= ex? ex[2].trim() : (x&&x.kind==='not-found'? 'The element is present and the step completes.' : x&&x.kind==='not-visible'? 'The element is visible.' : x&&/^(network|navigation)$/.test(x.kind)? 'The page loads.' : x&&x.kind==='api'? 'The API responds successfully.' : 'The step completes and the test passes.');
1456
+ const actual= rc? rc[2].trim() : (x? x.summary : msg.split('\n')[0]);
1457
+ return { t, r, e, x, si, repro, env, dataBlocks, api, logs, att, expected, actual,
1458
+ title: t.title+(x? ' – '+x.label.toLowerCase() : ''),
1459
+ where: t.file+':'+t.line+(data.projects.length>1?' ['+t.project+']':''),
1460
+ history: si? (si.kind==='new'? 'New failure in this run' : 'Failing since '+runLabel(si.since)) : (t.outcome==='flaky'? 'Flaky: passed on retry' : ''),
1461
+ attempts: t.results.length, when: new Date(data.startTime).toLocaleString() };
1462
+ }
1463
+ function bugReport(t, fmt){
1464
+ const m=bugModel(t), L=[];
1465
+ const H=(txt)=> fmt==='jira'? 'h3. '+txt : fmt==='md'? '### '+txt : txt.toUpperCase();
1466
+ const B=(txt)=> fmt==='text'? txt : '*'+txt+'*';
1467
+ const FENCE=String.fromCharCode(96,96,96);
1468
+ const code=(txt)=> fmt==='jira'? ['{code}',txt,'{code}'] : fmt==='md'? [FENCE,txt,FENCE] : [txt];
1469
+ const li=(i,txt)=> (fmt==='jira'? '# ' : (i+1)+'. ')+txt;
1470
+ const bullet=(txt)=> (fmt==='jira'? '* ' : '- ')+txt;
1471
+ const kv=(k,v)=> fmt==='text'? k+': '+v : (fmt==='jira'? '*'+k+':* ' : '**'+k+':** ')+v;
1472
+ L.push(fmt==='jira'? 'h2. '+m.title : fmt==='md'? '## '+m.title : m.title, '');
1473
+ const pr=[m.t.meta.priority, m.t.meta.severity].filter(Boolean).join(' / ');
1474
+ if(pr) L.push(kv('Priority', pr)); if(m.t.meta.owner) L.push(kv('Owner', m.t.meta.owner)); if(m.t.meta.feature) L.push(kv('Feature', m.t.meta.feature));
1475
+ for(const k of ['story','epic','issue']) if(m.t.meta[k]) L.push(kv(k[0].toUpperCase()+k.slice(1), m.t.meta[k]));
1476
+ L.push(kv('Test', m.where), kv('Run', m.when+(m.attempts>1?' · failed '+m.attempts+' attempts':'')+(m.history?' · '+m.history:'')), '');
1477
+ if(m.x){ L.push(H('Summary'), m.x.summary, ''); }
1478
+ if(m.env.length){ L.push(H('Environment')); for(const [k,v] of m.env) L.push(bullet(k+': '+v)); L.push(''); }
1479
+ if(m.dataBlocks.length){ L.push(H('Test data')); for(const b of m.dataBlocks){ L.push(B(b.name)); for(const ln of b.lines) L.push(bullet(ln)); } L.push(''); }
1480
+ L.push(H('Steps to reproduce'));
1481
+ if(m.repro.length) m.repro.forEach((s,i)=>L.push(li(i, s.title+(s.failed?' ← fails here':'')))); else L.push(li(0,'Run the test: '+m.where));
1482
+ L.push('', H('Expected result'), m.expected, '', H('Actual result'), m.actual, '');
1483
+ if(m.e){ L.push(H('Error')); L.push(...code(m.e.message.split('\n').slice(0,12).join('\n').trim())); L.push(''); }
1484
+ if(m.api.length){ L.push(H('API calls')); for(const a of m.api) L.push(bullet(a)); L.push(''); }
1485
+ if(m.logs.length){ L.push(H('Test log')); L.push(...code(m.logs.join('\n'))); L.push(''); }
1486
+ if(m.att.length){ L.push(H('Attachments')); L.push('In the report next to this test: '+m.att.join(', ')); L.push(''); }
1487
+ L.push(fmt==='text'? 'Generated by reportingLabs' : '_Generated by reportingLabs_');
1488
+ return L.join('\n').replace(/\n{3,}/g,'\n\n');
1489
+ }
1490
+ function openBugReport(t){
1491
+ let fmt=localStorage.getItem('rl-bug-fmt')||'md';
1492
+ const ta=h('textarea',{class:'bug-ta',spellcheck:'false'});
1493
+ const render=()=>{ ta.value=bugReport(t,fmt); ta.scrollTop=0; seg.querySelectorAll('button').forEach(b=>b.setAttribute('aria-pressed',b.dataset.f===fmt)); };
1494
+ const seg=h('div',{class:'seg'}, [['md','Markdown'],['jira','Jira'],['text','Plain text']].map(([f,l])=>h('button',{'data-f':f,onclick:()=>{fmt=f; try{localStorage.setItem('rl-bug-fmt',f);}catch(e){} render();}},l)));
1495
+ const dlg=h('div',{class:'bugdlg',onclick:e=>{ if(e.target===dlg) dlg.remove(); }},
1496
+ h('div',{class:'bug-box',role:'dialog','aria-label':'Bug report'},
1497
+ h('div',{class:'bug-head'}, h('h3',{},'Bug report'), h('span',{class:'hint'},'Edit the text if you like, then copy it into Jira, GitHub, Azure DevOps or an email.'), h('button',{class:'btn',onclick:()=>dlg.remove()},'Close')),
1498
+ h('div',{class:'bug-tools'}, seg, h('div',{class:'grow'}), h('button',{class:'btn',onclick:()=>download((fileStem()+'-'+t.title.replace(/[^a-z0-9]+/gi,'-').toLowerCase()).slice(0,80)+(fmt==='md'?'.md':'.txt'), ta.value, 'text/plain')},'Download'), h('button',{class:'btn primary',onclick:e=>copyText(ta.value,e.currentTarget,'Copied')},'Copy')),
1499
+ ta));
1500
+ render(); document.body.append(dlg); ta.scrollTop=0; try{ ta.focus({preventScroll:true}); ta.setSelectionRange(0,0); }catch(e){}
1501
+ }
1432
1502
  function lightbox(src){ const lb=h('div',{class:'lb',onclick:()=>lb.remove()}, h('img',{src})); document.body.append(lb); }
1433
1503
  function escape(s){ return s.replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
1434
1504
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reporting-labs",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "reportingLabs – beautiful, customizable single-file HTML test reports. One line of config. Playwright adapter included, more runners on the way.",
5
5
  "keywords": [
6
6
  "test-report",