reporting-labs 0.6.10 → 0.6.11
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 +35 -4
- package/dist/reporter.d.ts +15 -0
- package/dist/reporter.js +31 -0
- package/dist/template.html +237 -7
- package/dist/template.js +237 -7
- package/dist/types.d.ts +7 -0
- package/dist/wdio/env.d.ts +4 -0
- package/dist/wdio/env.js +62 -0
- package/dist/wdio/finalize.d.ts +38 -0
- package/dist/wdio/finalize.js +203 -0
- package/dist/wdio/index.d.ts +17 -0
- package/dist/wdio/index.js +22 -0
- package/dist/wdio/reporter.d.ts +67 -0
- package/dist/wdio/reporter.js +332 -0
- package/package.json +11 -2
package/dist/template.js
CHANGED
|
@@ -718,6 +718,104 @@ html,body{font-size:var(--fs)}
|
|
|
718
718
|
.nav .cnt,.chip,.step .cat,.folder,.item .d,.dim .k,.wk .n,.wk-sum,.attn2 .pr,.clu2 .n small,.flk .m,.own .cnts,.ediff .lbl,.ttip .tt-h span,.tags button,.hint-kbd,.trace-how,.api-col h5,.meta .k,.kv.env dt,.apitbl th,.hm thead th,.hm td button{font-family:var(--sans)}
|
|
719
719
|
.chip b,.loc,.file,.file-row,.kbd,.slow .d,.step .d,.hist .l,.wk .k,.tbl td,.kv2 dd,.kv.env dd,.logs,.txt,.err,.ediff .val,.ediff.lines,.clu2 .msg,.apitbl .u,.api-head .m,.api-head .sc,.api-head .d,.api-col pre,.badge.tag,.trend text,.tl text,.attn2 .tt .s .mono,.trace-how code{font-family:var(--mono)}
|
|
720
720
|
.chip{font-family:var(--sans)} .chip b{font-weight:500}
|
|
721
|
+
|
|
722
|
+
/* ---------- report footer: reportingLabs branding ---------- */
|
|
723
|
+
.rl-foot{max-width:1400px;margin:30px auto 0;padding:18px 28px 30px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px;color:var(--ink-3);font-size:12.5px}
|
|
724
|
+
.rl-foot-mark{width:22px;height:22px;display:inline-flex;flex:none}
|
|
725
|
+
.rl-foot-mark svg{width:22px;height:22px;display:block;border-radius:6px}
|
|
726
|
+
.rl-foot-txt b{color:var(--ink-2);font-weight:600}
|
|
727
|
+
@media print{ .rl-foot{display:none} }
|
|
728
|
+
|
|
729
|
+
/* ---------- print / PDF: a clean, client-shareable executive report ----------
|
|
730
|
+
Page 1 is a full executive summary (no wasted space); analytics, failure analysis, the
|
|
731
|
+
full test-case table and failure evidence each start on a fresh page. Per-test steps,
|
|
732
|
+
logs, API and console are left out so the PDF stays small and readable at 500+ tests. */
|
|
733
|
+
.printdoc{display:none}
|
|
734
|
+
@media print{
|
|
735
|
+
@page{ size:A4; margin:12mm 11mm 13mm; }
|
|
736
|
+
html,body{background:#fff!important}
|
|
737
|
+
#app{display:none!important}
|
|
738
|
+
.printdoc{display:block!important;max-width:none;margin:0;padding:0;color:var(--ink);font-size:12px}
|
|
739
|
+
.printdoc *{ -webkit-print-color-adjust:exact!important; print-color-adjust:exact!important; }
|
|
740
|
+
/* On paper nothing can be clicked or hovered, so never truncate text with an ellipsis or a
|
|
741
|
+
height clamp — wrap it and show it in full. Numbers, durations and status chips stay on one line. */
|
|
742
|
+
.printdoc .apitbl .u,.printdoc .apitbl .t,.printdoc .apitbl td,.printdoc .slow .t,.printdoc .proj .name,
|
|
743
|
+
.printdoc .file,.printdoc .item .tt .p,.printdoc .item .tt .n,.printdoc .item .d,.printdoc .step .t,
|
|
744
|
+
.printdoc .att figcaption,.printdoc .dim .k,.printdoc .attn .t,.printdoc .attn .t .tt,.printdoc .attn .sev,
|
|
745
|
+
.printdoc .attn2 .pr,.printdoc .attn2 .ow,.printdoc .attn2 .tt .n,.printdoc .attn2 .tt .s,
|
|
746
|
+
.printdoc .tbl td,.printdoc .tbl th,.printdoc .api-head .u,.printdoc .clu .msg,.printdoc .clu .n,
|
|
747
|
+
.printdoc .clu2 .msg,.printdoc .clu2 .who,.printdoc .clu2 .who button,.printdoc .own .ts,.printdoc .own .nm,
|
|
748
|
+
.printdoc .hist .b .l,.printdoc .loc,.printdoc .crumb span,.printdoc .pt-name,.printdoc .pt-sub,.printdoc .pt-spec{
|
|
749
|
+
white-space:normal!important;overflow:visible!important;text-overflow:clip!important;
|
|
750
|
+
max-width:none!important;max-height:none!important;overflow-wrap:break-word;-webkit-line-clamp:none!important;
|
|
751
|
+
}
|
|
752
|
+
/* durations / counts / status stay on one line so "9ms" never breaks to "9m s" */
|
|
753
|
+
.printdoc .apitbl td.t:last-child,.printdoc .num,.printdoc .pstatus,.printdoc .since-txt{white-space:nowrap!important;overflow-wrap:normal!important}
|
|
754
|
+
/* only genuinely unbreakable strings (urls, file paths, selectors) may break mid-token */
|
|
755
|
+
.printdoc .apitbl .u,.printdoc .api-head .u,.printdoc .pt-spec,.printdoc .loc,.printdoc .file,.printdoc .clu .msg,.printdoc .clu2 .msg{overflow-wrap:anywhere}
|
|
756
|
+
/* Content flows continuously — no forced page breaks between sections, which were leaving
|
|
757
|
+
whole pages nearly empty. Cards/rows just avoid being split across a page. */
|
|
758
|
+
.printdoc .psec{padding-top:0;margin-top:18px}
|
|
759
|
+
.printdoc .psec-h{font-size:16px;font-weight:600;letter-spacing:-.01em;margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid var(--line);color:var(--ink);break-after:avoid}
|
|
760
|
+
/* Only small, atomic blocks avoid a page split. Big cards (clusters, needs-attention, the tables)
|
|
761
|
+
may flow across pages — keeping them whole is what left pages half-empty. */
|
|
762
|
+
.printdoc .gchart,.printdoc figure,.printdoc .kpi,.printdoc .hm,.printdoc .pev,.printdoc .pexec-card,.printdoc tr{break-inside:avoid}
|
|
763
|
+
.printdoc h4{break-after:avoid}
|
|
764
|
+
/* CSS grid does not fragment across print pages, so a tall two-column grid jumps to the next page
|
|
765
|
+
and leaves a gap. In print everything becomes a single full-width column that flows naturally. */
|
|
766
|
+
.printdoc .grid{display:block!important;margin:0}
|
|
767
|
+
.printdoc .grid>*{grid-column:auto!important;width:auto!important;max-width:none!important;margin:0 0 12px!important;break-inside:avoid}
|
|
768
|
+
.printdoc .grid>.card.w12,.printdoc .grid>.fail-rail,.printdoc .grid>.fx-tools{break-inside:auto}
|
|
769
|
+
.printdoc .graphs-grid>*{break-inside:avoid}
|
|
770
|
+
.printdoc .list,.printdoc .nav,.printdoc .band,.printdoc .stripe,.printdoc .fx-tools .btn,.printdoc .fx-tools .spacer{display:none!important}
|
|
771
|
+
/* drop interactive-only chrome and anything that cannot live on paper */
|
|
772
|
+
.printdoc .actions,.printdoc .gchart-head button,.printdoc .vids,.printdoc .cmp,.printdoc .hint-kbd,.printdoc .mask-note,.printdoc .clu2 .who .ex{display:none!important}
|
|
773
|
+
.printdoc a{color:var(--ink);text-decoration:none}
|
|
774
|
+
/* ---- cover / executive summary (page 1) ---- */
|
|
775
|
+
.printdoc .pcover{padding:2mm 0 0}
|
|
776
|
+
.printdoc .pcover-top{display:flex;align-items:center;gap:13px}
|
|
777
|
+
.printdoc .pcover .logo{width:44px;height:44px;flex:none}
|
|
778
|
+
.printdoc .pcover .logo svg{width:44px;height:44px;display:block;border-radius:11px}
|
|
779
|
+
.printdoc .pcover-title{font-size:25px;font-weight:600;letter-spacing:-.02em;line-height:1.1}
|
|
780
|
+
.printdoc .pcover-sub{color:var(--ink-3);font-size:12px;margin-top:3px}
|
|
781
|
+
.printdoc .pcover-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
|
782
|
+
.printdoc .pverdict{font-size:17px;font-weight:600;margin:16px 0 2px}
|
|
783
|
+
.printdoc .pverdict.ok{color:var(--pass)} .printdoc .pverdict.bad{color:var(--fail)}
|
|
784
|
+
.printdoc .pcover-stats{display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 4px}
|
|
785
|
+
.printdoc .pstat{border:1px solid var(--line);border-radius:10px;padding:9px 15px;min-width:84px;background:var(--surface)}
|
|
786
|
+
.printdoc .pstat .n{font-size:25px;font-weight:600;font-variant-numeric:tabular-nums;line-height:1.1}
|
|
787
|
+
.printdoc .pstat .l{font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em;margin-top:3px}
|
|
788
|
+
.printdoc .pexec{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px;align-items:start}
|
|
789
|
+
.printdoc .pexec-card{border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--surface)}
|
|
790
|
+
.printdoc .psub-h{font-size:12.5px;font-weight:600;color:var(--ink);margin:0 0 10px}
|
|
791
|
+
/* ---- the all-tests table ---- */
|
|
792
|
+
.printdoc .ptbl{width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed}
|
|
793
|
+
.printdoc .ptbl thead{display:table-header-group}
|
|
794
|
+
.printdoc .ptbl th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-3);font-weight:600;padding:7px 8px;border-bottom:2px solid var(--line)}
|
|
795
|
+
.printdoc .ptbl td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}
|
|
796
|
+
.printdoc .ptbl td.num{text-align:right;font-variant-numeric:tabular-nums;color:var(--ink-2);white-space:nowrap}
|
|
797
|
+
.printdoc .ptbl .pt-name{font-weight:500;color:var(--ink)}
|
|
798
|
+
.printdoc .ptbl .pt-sub{font-size:10px;color:var(--ink-3);margin-top:1px}
|
|
799
|
+
.printdoc .ptbl .pt-spec{font:10.5px var(--mono);color:var(--ink-2)}
|
|
800
|
+
.printdoc .pstatus{display:inline-flex;align-items:center;gap:6px;font-weight:500;white-space:nowrap}
|
|
801
|
+
.printdoc .pstatus .st{width:9px;height:9px;border-radius:50%;flex:none}
|
|
802
|
+
.printdoc .ptbl .c-n{width:30px} .printdoc .ptbl .c-pri{width:72px} .printdoc .ptbl .c-own{width:84px} .printdoc .ptbl .c-st{width:88px} .printdoc .ptbl .c-dur{width:62px}
|
|
803
|
+
/* ---- failure evidence ---- */
|
|
804
|
+
.printdoc .pev{padding:14px 0;border-top:1px solid var(--line)}
|
|
805
|
+
.printdoc .pev:first-child{border-top:0;padding-top:2px}
|
|
806
|
+
.printdoc .pev-head{display:flex;align-items:center;gap:9px}
|
|
807
|
+
.printdoc .pev-head .st{width:10px;height:10px;border-radius:50%;flex:none}
|
|
808
|
+
.printdoc .pev-title{font-size:14px;font-weight:600;letter-spacing:-.01em}
|
|
809
|
+
.printdoc .pev-loc{font:11px var(--mono);color:var(--ink-3);margin:3px 0 8px}
|
|
810
|
+
.printdoc .pev .why{margin-bottom:8px}
|
|
811
|
+
.printdoc .pev .att{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px}
|
|
812
|
+
.printdoc .pev .att figure{flex:1 1 320px;min-width:0;margin:0}
|
|
813
|
+
.printdoc .pev .att img{width:100%;max-height:115mm;object-fit:contain;border:1px solid var(--line);border-radius:6px;background:#fff}
|
|
814
|
+
.printdoc .pev .att figcaption{font:11px var(--mono);color:var(--ink-3);padding:5px 7px;background:var(--surface-2);border-radius:0 0 6px 6px;margin-top:-6px}
|
|
815
|
+
.printdoc .pev .errfull,.printdoc .pev details{display:none!important}
|
|
816
|
+
.printdoc .pfoot{margin-top:5mm;padding-top:3mm;border-top:1px solid var(--line);font-size:10px;color:var(--ink-3)}
|
|
817
|
+
.printdoc .err,.printdoc pre{white-space:pre-wrap;word-break:break-word}
|
|
818
|
+
}
|
|
721
819
|
`;
|
|
722
820
|
const JS = String.raw `
|
|
723
821
|
(function(){
|
|
@@ -750,7 +848,8 @@ app.append(h('div',{class:'band'}, stripe(), header(), nav()),
|
|
|
750
848
|
h('div',{class:'view','data-view':'failures'}, h('div',{class:'view-pad'}, failuresView())),
|
|
751
849
|
h('div',{class:'view','data-view':'api'}, h('div',{class:'view-pad'}, apiView())),
|
|
752
850
|
h('div',{class:'view','data-view':'graphs'}, graphsView()),
|
|
753
|
-
hasTimelineView()? h('div',{class:'view','data-view':'timeline'}, h('div',{class:'view-pad'}, timelineView())) : null
|
|
851
|
+
hasTimelineView()? h('div',{class:'view','data-view':'timeline'}, h('div',{class:'view-pad'}, timelineView())) : null,
|
|
852
|
+
rlFooter());
|
|
754
853
|
select(state.selected || firstInteresting(), true);
|
|
755
854
|
showView(state.view);
|
|
756
855
|
window.addEventListener('keydown', e=>{
|
|
@@ -1045,6 +1144,7 @@ function header(){
|
|
|
1045
1144
|
const pal = h('select',{class:'pal','aria-label':'Palette',onchange:e=>{ html.setAttribute('data-palette',e.target.value); try{localStorage.setItem('rl-palette',e.target.value);}catch(e){} }},
|
|
1046
1145
|
[['lab','Blue'],['ocean','Ocean'],['ember','Ember'],['mono','Mono']].map(([v,l])=>h('option',{value:v,selected:html.getAttribute('data-palette')===v},l)));
|
|
1047
1146
|
const copy = h('button',{class:'btn',onclick:e=>copyText(summaryMarkdown(),e.currentTarget,'Copied for Slack')}, 'Copy summary');
|
|
1147
|
+
const pdf = h('button',{class:'btn',title:'Save the whole report as a PDF — every section, chart, cluster and screenshot, print-friendly',onclick:()=>exportPdf()}, 'Export PDF');
|
|
1048
1148
|
return h('header',{class:'hdr'},
|
|
1049
1149
|
data.options.logo ? h('img',{class:'logo brand',src:data.options.logo,alt:''}) : h('div',{class:'logo',title:'Generated by reportingLabs','aria-label':'reportingLabs'}, document.getElementById('rl-logo').content.cloneNode(true)),
|
|
1050
1150
|
data.options.project ? h('div',{class:'proj-blk'}, h('h1',{}, data.title),
|
|
@@ -1055,7 +1155,17 @@ function header(){
|
|
|
1055
1155
|
h('div',{class:'hmeta'}, Object.entries(data.metadata).map(([k,v])=>h('span',{class:'chip'}, k+' ', h('b',{},v)))),
|
|
1056
1156
|
h('div',{class:'spacer'}),
|
|
1057
1157
|
h('span',{class:'when'}, new Date(data.startTime).toLocaleString()+' · '+ms(data.duration)+' · '+data.workers+' worker'+(data.workers===1?'':'s')),
|
|
1058
|
-
h('div',{class:'ctl'}, copy, pal, toggle));
|
|
1158
|
+
h('div',{class:'ctl'}, copy, pdf, pal, toggle));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/* ---------- footer: reportingLabs branding ---------- */
|
|
1162
|
+
function rlFooter(){
|
|
1163
|
+
const src=document.getElementById('rl-logo').content.cloneNode(true).querySelector('svg');
|
|
1164
|
+
const svg=src.outerHTML.replace(/(id="|url\(#|href="#)(rl-[a-z]+)/g,'$1$2-f'); // unique ids vs the header's copy
|
|
1165
|
+
return h('footer',{class:'rl-foot'},
|
|
1166
|
+
h('span',{class:'rl-foot-mark',html:svg}),
|
|
1167
|
+
h('span',{class:'rl-foot-txt'}, 'Generated by ', h('b',{},'reportingLabs'),
|
|
1168
|
+
data.generatedAt? ' · '+new Date(data.generatedAt).toLocaleString() : null));
|
|
1059
1169
|
}
|
|
1060
1170
|
|
|
1061
1171
|
/* ---------- summary ---------- */
|
|
@@ -1580,8 +1690,9 @@ function select(id, quiet){
|
|
|
1580
1690
|
state.selected=id; state.retry=t.results.length-1; if(!quiet){ if(state.view!=='tests') showView('tests'); else setHash(); } refresh(); renderDetail(t);
|
|
1581
1691
|
const it=document.querySelector('.item[data-id="'+id+'"]'); if(it&&it.scrollIntoView) it.scrollIntoView({block:'nearest'});
|
|
1582
1692
|
}
|
|
1583
|
-
function renderDetail(t){
|
|
1584
|
-
|
|
1693
|
+
function renderDetail(t, d, retryIdx){
|
|
1694
|
+
d=d||$('#detail'); d.innerHTML='';
|
|
1695
|
+
const retry = retryIdx==null ? state.retry : retryIdx;
|
|
1585
1696
|
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))),
|
|
1586
1697
|
h('div',{class:'titlebar'}, h('h3',{},t.title), h('div',{class:'actions'},
|
|
1587
1698
|
h('button',{class:'btn',onclick:e=>copyText(location.href.split('#')[0]+'#t='+t.id,e.currentTarget,'Link copied')},'Copy link'),
|
|
@@ -1594,11 +1705,11 @@ function renderDetail(t){
|
|
|
1594
1705
|
const otherAnn=t.annotations.filter(a=>!DIMS.includes(a.type.toLowerCase()) && !(a.type.toLowerCase() in t.meta));
|
|
1595
1706
|
if(otherAnn.length) d.append(h('h4',{},'Annotations'), h('dl',{class:'kv'}, otherAnn.map(a=>[h('dt',{},a.type),h('dd',{},a.description||'')])));
|
|
1596
1707
|
if(t.results.length>1){
|
|
1597
|
-
d.append(h('div',{class:'tabs'}, t.results.map((r,i)=>h('button',{class:'tab','aria-selected':
|
|
1708
|
+
d.append(h('div',{class:'tabs'}, t.results.map((r,i)=>h('button',{class:'tab','aria-selected':retry===i,onclick:()=>{state.retry=i;renderDetail(t);}}, (i===0?'Attempt 1':'Retry '+i)+' · '+(label[r.status]||r.status)))));
|
|
1598
1709
|
}
|
|
1599
|
-
const r=t.results[
|
|
1710
|
+
const r=t.results[retry]; if(!r){ d.append(h('p',{class:'empty'},'This test did not run.')); return; }
|
|
1600
1711
|
const body=h('div',{});
|
|
1601
|
-
if(t.note&&
|
|
1712
|
+
if(t.note&&retry===t.results.length-1) body.append(h('div',{class:'note '+(t.outcome==='passed'?'ok':'warn')}, t.note));
|
|
1602
1713
|
if(r.errors.length){ body.append(h('h4',{},r.errors.length>1?'Errors':'Error'), ...r.errors.map(errorView)); }
|
|
1603
1714
|
if(r.steps.length){ const stepTotal=Math.max(1, r.duration||0, r.steps.reduce((a,x)=>a+x.duration,0)); body.append(h('h4',{},data.bdd?'Scenario steps':'Steps', h('span',{class:'hint'},'bar = share of '+ms(stepTotal))), stepTree(r.steps, stepTotal)); }
|
|
1604
1715
|
if(r.logs.length){ const t0=r.startTime; body.append(h('h4',{},'Log'), h('div',{class:'logs'}, r.logs.map(l=>h('div',{class:'ln'+(/\b(error|fail|exception)\b/i.test(l.msg)?' err':/\bwarn/i.test(l.msg)?' warn':'')}, h('span',{class:'ts'},'+'+ms(Math.max(0,l.t-t0))), h('span',{class:'lm'},l.msg))))); }
|
|
@@ -1754,5 +1865,124 @@ function openBugReport(t){
|
|
|
1754
1865
|
}
|
|
1755
1866
|
function lightbox(src){ const lb=h('div',{class:'lb',onclick:()=>lb.remove()}, h('img',{src})); document.body.append(lb); }
|
|
1756
1867
|
function escape(s){ return s.replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
1868
|
+
|
|
1869
|
+
/* ---------- print / PDF ----------
|
|
1870
|
+
One builder serves both the "Export PDF" button (window.print) and headless page.pdf():
|
|
1871
|
+
Chromium fires 'beforeprint' for both. We assemble the whole report — cover, overview,
|
|
1872
|
+
failures, graphs, API and every test's detail expanded — into a .printdoc, force the light
|
|
1873
|
+
theme for paper, print, then tear it all down so the live app is untouched. Video is skipped. */
|
|
1874
|
+
function buildPrintDoc(){
|
|
1875
|
+
if(document.querySelector('.printdoc')) return;
|
|
1876
|
+
const html=document.documentElement;
|
|
1877
|
+
html.setAttribute('data-rl-theme-prev', html.getAttribute('data-theme')||'');
|
|
1878
|
+
html.setAttribute('data-theme','light'); // always light on paper, whatever the screen theme
|
|
1879
|
+
const pd=h('div',{class:'printdoc'});
|
|
1880
|
+
const s=data.stats, f=s.failed+s.timedOut+s.interrupted, ran=s.total-s.skipped, rate=ran?Math.round(s.passed/ran*100):0;
|
|
1881
|
+
const stat=(n,l,c)=>h('div',{class:'pstat'}, h('div',{class:'n',style:c?'color:'+c:''},String(n)), h('div',{class:'l'},l));
|
|
1882
|
+
let logo;
|
|
1883
|
+
if(data.options.logo){ logo = h('img',{src:data.options.logo,alt:'',style:'width:40px;height:40px;border-radius:8px'}); }
|
|
1884
|
+
else {
|
|
1885
|
+
// The live header already holds one copy of this SVG; in print that copy is display:none, and its
|
|
1886
|
+
// gradient/clip ids would win over an identical clone (first id in the document). Rewrite the ids so
|
|
1887
|
+
// the cover's logo paints its gradients and flask clip correctly.
|
|
1888
|
+
const src=document.getElementById('rl-logo').content.cloneNode(true).querySelector('svg');
|
|
1889
|
+
const html2=src.outerHTML.replace(/(id="|url\(#|href="#)(rl-[a-z]+)/g,'$1$2-p');
|
|
1890
|
+
const holder=h('span',{class:'logo',style:'display:inline-flex;width:40px;height:40px',html:html2});
|
|
1891
|
+
logo=holder;
|
|
1892
|
+
}
|
|
1893
|
+
const verdict = f ? (f+' of '+s.total+' test'+(s.total===1?'':'s')+' failed'+(s.flaky?', '+s.flaky+' flaky':''))
|
|
1894
|
+
: (ran===0 ? 'No tests ran' : 'All '+ran+' test'+(ran===1?'':'s')+' passed'+(s.flaky?', '+s.flaky+' flaky':'')+(s.skipped?' · '+s.skipped+' skipped':''));
|
|
1895
|
+
// ---- page 1: executive summary ----
|
|
1896
|
+
pd.append(h('div',{class:'pcover'},
|
|
1897
|
+
h('div',{class:'pcover-top'}, logo,
|
|
1898
|
+
h('div',{}, h('div',{class:'pcover-title'}, data.title),
|
|
1899
|
+
h('div',{class:'pcover-sub'}, (data.options.project?[data.options.project.name, data.options.project.version?'v'+data.options.project.version:null, data.options.project.team].filter(Boolean).join(' · ')+' · ':'')
|
|
1900
|
+
+ new Date(data.startTime).toLocaleString()+' · '+ms(data.duration)+' · '+data.workers+' worker'+(data.workers===1?'':'s')))),
|
|
1901
|
+
Object.keys(data.metadata).length? h('div',{class:'pcover-meta'}, Object.entries(data.metadata).map(([k,v])=>h('span',{class:'chip'}, k+' ', h('b',{},v)))) : null,
|
|
1902
|
+
h('div',{class:'pverdict '+(f?'bad':'ok')}, verdict),
|
|
1903
|
+
h('div',{class:'pcover-stats'},
|
|
1904
|
+
stat(rate+'%','Pass rate'), stat(s.passed,'Passed','var(--pass)'),
|
|
1905
|
+
f?stat(f,'Failed','var(--fail)'):null, s.flaky?stat(s.flaky,'Flaky','var(--flaky)'):null,
|
|
1906
|
+
s.skipped?stat(s.skipped,'Skipped','var(--ink-3)'):null, stat(s.total,'Total'))));
|
|
1907
|
+
const pexec=h('div',{class:'pexec'});
|
|
1908
|
+
pexec.append(h('div',{class:'pexec-card'}, h('div',{class:'psub-h'},'Pass rate'), donut()));
|
|
1909
|
+
const envc=printEnvCard(); if(envc) pexec.append(envc);
|
|
1910
|
+
pd.append(pexec);
|
|
1911
|
+
|
|
1912
|
+
// The first section flows right after the executive summary so page 1 is never half-empty;
|
|
1913
|
+
// the rest start on a fresh page.
|
|
1914
|
+
let first=true;
|
|
1915
|
+
const sec=(title,node)=>{ if(!node) return; pd.append(h('section',{class:'psec'+(first?'':' brk')}, h('h2',{class:'psec-h'},title), node)); first=false; };
|
|
1916
|
+
// ---- analytics, failure analysis, full inventory, evidence ----
|
|
1917
|
+
if(data.tests.length) sec('Test analytics', graphsView());
|
|
1918
|
+
if(f + s.flaky > 0){
|
|
1919
|
+
const fn=failuresView();
|
|
1920
|
+
// On paper there is no "+N more" to click, so expand every cluster to list all its tests.
|
|
1921
|
+
fn.querySelectorAll('.clu2 .who .ex').forEach(b=>{ try{ b.click(); }catch(e){} });
|
|
1922
|
+
sec('Failure analysis', fn);
|
|
1923
|
+
}
|
|
1924
|
+
sec('All test cases', allTestsTable());
|
|
1925
|
+
const bad=[...data.tests].filter(t=>isFail(t.outcome)||t.outcome==='flaky').sort((a,b)=>rank(a)-rank(b)||a.title.localeCompare(b.title));
|
|
1926
|
+
if(bad.length){ const ev=h('div',{}); for(const t of bad) ev.append(printEvidence(t)); sec('Failure evidence', ev); }
|
|
1927
|
+
pd.append(h('div',{class:'pfoot'}, 'Generated by reportingLabs · '+new Date().toLocaleString()));
|
|
1928
|
+
document.body.append(pd);
|
|
1929
|
+
}
|
|
1930
|
+
function printEnvCard(){
|
|
1931
|
+
if(!data.env||!data.env.length) return null;
|
|
1932
|
+
return h('div',{class:'pexec-card'}, h('div',{class:'psub-h'},'Environment'),
|
|
1933
|
+
h('dl',{class:'kv env'}, data.env.flatMap(r=>[h('dt',{},r.k), h('dd',{}, r.href? h('a',{href:r.href},r.v) : r.v)])));
|
|
1934
|
+
}
|
|
1935
|
+
function allTestsTable(){
|
|
1936
|
+
const hasPri=data.tests.some(t=>t.meta.priority||t.meta.severity), hasOwn=data.tests.some(t=>t.meta.owner), multiProj=data.projects.length>1;
|
|
1937
|
+
const cols=[['c-n','#'],['','Test'],['','Spec'], hasPri?['c-pri','Priority']:null, hasOwn?['c-own','Owner']:null, ['c-st','Status'], ['c-dur','Duration']].filter(Boolean);
|
|
1938
|
+
const order=[...data.tests].sort((a,b)=>((isFail(b.outcome)||b.outcome==='flaky')-(isFail(a.outcome)||a.outcome==='flaky'))||rank(a)-rank(b)||a.file.localeCompare(b.file)||a.title.localeCompare(b.title));
|
|
1939
|
+
let i=0;
|
|
1940
|
+
return h('table',{class:'ptbl'},
|
|
1941
|
+
h('colgroup',{}, cols.map(([c])=>h('col',{class:c||''}))),
|
|
1942
|
+
h('thead',{}, h('tr',{}, cols.map(([,l])=>h('th',{},l)))),
|
|
1943
|
+
h('tbody',{}, order.map(t=>{ i++; return h('tr',{}, [
|
|
1944
|
+
h('td',{class:'num'}, String(i)),
|
|
1945
|
+
h('td',{}, h('div',{class:'pt-name'}, t.title), t.path.length? h('div',{class:'pt-sub'}, t.path.join(' › ')) : null),
|
|
1946
|
+
h('td',{}, h('span',{class:'pt-spec'}, t.file.split('/').pop()+(multiProj?' · '+t.project:''))),
|
|
1947
|
+
hasPri? h('td',{}, [t.meta.priority,t.meta.severity].filter(Boolean).join(' · ')) : null,
|
|
1948
|
+
hasOwn? h('td',{}, t.meta.owner||'') : null,
|
|
1949
|
+
h('td',{}, h('span',{class:'pstatus'}, h('span',{class:'st '+t.outcome,style:'background:'+colorOf(bucket(t))}), label[t.outcome])),
|
|
1950
|
+
h('td',{class:'num'}, ms(t.duration)),
|
|
1951
|
+
].filter(x=>x!==null)); })));
|
|
1952
|
+
}
|
|
1953
|
+
function printEvidence(t){
|
|
1954
|
+
const r=t.results[t.results.length-1], e=r&&r.errors[0];
|
|
1955
|
+
const art=h('article',{class:'pev'});
|
|
1956
|
+
art.append(h('div',{class:'pev-head'}, h('span',{class:'st',style:'background:'+colorOf(bucket(t))}), h('span',{class:'pev-title'}, t.title), h('span',{class:'badge '+t.outcome}, label[t.outcome])));
|
|
1957
|
+
art.append(h('div',{class:'pev-loc'}, t.file+':'+t.line+(data.projects.length>1?' · '+t.project:'')+' · '+ms(t.duration)+(t.results.length>1?' · '+t.results.length+' attempts':'')+(t.meta.owner?' · '+t.meta.owner:'')+(t.meta.priority?' · '+t.meta.priority:'')));
|
|
1958
|
+
if(e){ if(e.explain) art.append(whyView(e.explain)); art.append(errorBody(e)); }
|
|
1959
|
+
else art.append(h('div',{style:'color:var(--ink-3)'}, t.outcome==='flaky'?'Passed on retry after an earlier failure.':'No error message was recorded.'));
|
|
1960
|
+
const imgs=(r?r.attachments:[]).filter(a=>a.src&&(a.contentType||'').startsWith('image/')&&!/-(expected|actual|diff)(\.\w+)?$/.test(a.name));
|
|
1961
|
+
if(imgs.length) art.append(h('div',{class:'att'}, imgs.slice(0,2).map(a=>h('figure',{}, h('img',{src:a.src,alt:a.name}), h('figcaption',{},a.name)))));
|
|
1962
|
+
return art;
|
|
1963
|
+
}
|
|
1964
|
+
function teardownPrintDoc(){
|
|
1965
|
+
const pd=document.querySelector('.printdoc'); if(pd) pd.remove();
|
|
1966
|
+
const html=document.documentElement, prev=html.getAttribute('data-rl-theme-prev');
|
|
1967
|
+
if(prev!=null){ if(prev) html.setAttribute('data-theme',prev); else html.removeAttribute('data-theme'); html.removeAttribute('data-rl-theme-prev'); }
|
|
1968
|
+
}
|
|
1969
|
+
// Build the print document and wait for every screenshot (a data: URI) to actually decode, so it
|
|
1970
|
+
// is painted — otherwise a print fired right after the DOM is built captures blank image boxes.
|
|
1971
|
+
async function preparePrint(){
|
|
1972
|
+
buildPrintDoc();
|
|
1973
|
+
try{ await Promise.all([...document.querySelectorAll('.printdoc img')].map(im=> im.decode? im.decode().catch(()=>{}) : Promise.resolve())); }catch(e){}
|
|
1974
|
+
return document.querySelectorAll('.printdoc img').length;
|
|
1975
|
+
}
|
|
1976
|
+
async function exportPdf(){
|
|
1977
|
+
await preparePrint();
|
|
1978
|
+
try{ window.print(); }catch(e){}
|
|
1979
|
+
}
|
|
1980
|
+
// A headless driver (the reporter's auto-generated report.pdf) awaits this, then calls page.pdf().
|
|
1981
|
+
window.reportingLabsPreparePrint = preparePrint;
|
|
1982
|
+
// A plain headless Chrome launched with --print-to-pdf on this file with ?rl-print (how the Java/CLI
|
|
1983
|
+
// generator renders report.pdf) has no way to call the hook, so build the print document on load instead.
|
|
1984
|
+
if(/[?&#]rl-print\b/.test(location.href)){ if(document.readyState!=='loading') preparePrint(); else window.addEventListener('DOMContentLoaded', preparePrint); }
|
|
1985
|
+
window.addEventListener('beforeprint', buildPrintDoc); // native Ctrl+P: build if not already built
|
|
1986
|
+
window.addEventListener('afterprint', teardownPrintDoc);
|
|
1757
1987
|
})();
|
|
1758
1988
|
`;
|
package/dist/types.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface ReportingLabsOptions {
|
|
|
17
17
|
emitJson?: boolean;
|
|
18
18
|
/** File name of the JSON blob inside outputFolder. Default: "report.json" */
|
|
19
19
|
jsonFile?: string;
|
|
20
|
+
/** Also write a print-ready `report.pdf` next to the HTML, rendered by headless Chromium from the
|
|
21
|
+
* report's own PDF layout (executive summary, charts, failure analysis, the full test-case table and
|
|
22
|
+
* screenshots of failures). Needs the Chromium that Playwright already provides. Default: true —
|
|
23
|
+
* set `false` to skip, or `{ file }` to rename. The report also has an "Export PDF" button. */
|
|
24
|
+
pdf?: boolean | {
|
|
25
|
+
file?: string;
|
|
26
|
+
};
|
|
20
27
|
/** Inline screenshots as base64 (single file, opens anywhere). Default: true */
|
|
21
28
|
embedAttachments?: boolean;
|
|
22
29
|
/** Max size (bytes) of a single attachment to embed. Larger ones are copied as files. Default: 2 MB */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ciRunLabel } from '../reporter';
|
|
2
|
+
import type { EnvRow } from '../types';
|
|
3
|
+
export { ciRunLabel };
|
|
4
|
+
export declare function collectEnv(base: string, extra: Record<string, string> | undefined, browsers: string[], workers: number, metadataBranch?: string): EnvRow[];
|
package/dist/wdio/env.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ciRunLabel = void 0;
|
|
37
|
+
exports.collectEnv = collectEnv;
|
|
38
|
+
/** Environment-card rows for the WDIO report, reusing the Playwright reporter's CI/git helpers. */
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const reporter_1 = require("../reporter");
|
|
41
|
+
Object.defineProperty(exports, "ciRunLabel", { enumerable: true, get: function () { return reporter_1.ciRunLabel; } });
|
|
42
|
+
function collectEnv(base, extra, browsers, workers, metadataBranch) {
|
|
43
|
+
const env = process.env;
|
|
44
|
+
const rows = [];
|
|
45
|
+
rows.push({ k: 'Node', v: process.version });
|
|
46
|
+
rows.push({ k: 'OS', v: `${os.type()} ${os.release()} (${os.arch()})` });
|
|
47
|
+
if (browsers.length)
|
|
48
|
+
rows.push({ k: 'Browsers', v: browsers.join(', ') });
|
|
49
|
+
if (workers > 1)
|
|
50
|
+
rows.push({ k: 'Workers', v: String(workers) });
|
|
51
|
+
const ci = (0, reporter_1.ciLink)(env);
|
|
52
|
+
if (ci)
|
|
53
|
+
rows.push({ k: 'CI', v: ci.name, href: ci.url });
|
|
54
|
+
const git = (0, reporter_1.gitInfo)(base, env);
|
|
55
|
+
if (git.sha)
|
|
56
|
+
rows.push({ k: 'Commit', v: `${git.sha.slice(0, 7)}${git.author ? ' · ' + git.author : ''}${git.subject ? ' · ' + git.subject : ''}`, href: git.url });
|
|
57
|
+
if (git.branch && !metadataBranch)
|
|
58
|
+
rows.push({ k: 'Branch', v: git.branch });
|
|
59
|
+
for (const [k, v] of Object.entries(extra || {}))
|
|
60
|
+
rows.push({ k, v: String(v), href: /^https?:\/\//.test(String(v)) ? String(v) : undefined });
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ReportingLabsCompleteOptions {
|
|
2
|
+
outputFolder?: string;
|
|
3
|
+
outputFile?: string;
|
|
4
|
+
jsonFile?: string;
|
|
5
|
+
emitJson?: boolean;
|
|
6
|
+
pdf?: boolean | {
|
|
7
|
+
file?: string;
|
|
8
|
+
};
|
|
9
|
+
title?: string;
|
|
10
|
+
logo?: string;
|
|
11
|
+
accent?: string;
|
|
12
|
+
theme?: 'auto' | 'light' | 'dark';
|
|
13
|
+
palette?: 'lab' | 'ocean' | 'ember' | 'mono';
|
|
14
|
+
metadata?: Record<string, string>;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
sections?: {
|
|
17
|
+
title: string;
|
|
18
|
+
html: string;
|
|
19
|
+
}[];
|
|
20
|
+
customCss?: string;
|
|
21
|
+
dimensions?: string[];
|
|
22
|
+
dimensionOrder?: Record<string, string[]>;
|
|
23
|
+
project?: unknown;
|
|
24
|
+
links?: Record<string, unknown>;
|
|
25
|
+
bdd?: boolean;
|
|
26
|
+
announce?: boolean;
|
|
27
|
+
history?: {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
file?: string;
|
|
30
|
+
keep?: number;
|
|
31
|
+
};
|
|
32
|
+
maskKeys?: string[];
|
|
33
|
+
maskValues?: string[];
|
|
34
|
+
maskFromEnv?: boolean;
|
|
35
|
+
embedFonts?: boolean;
|
|
36
|
+
editorLinks?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function reportingLabsComplete(options?: ReportingLabsCompleteOptions): Promise<string | undefined>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.reportingLabsComplete = reportingLabsComplete;
|
|
37
|
+
/**
|
|
38
|
+
* Combines the per-runner partials written by the WDIO reporter into the one report —
|
|
39
|
+
* index.html, report.json and (best-effort) report.pdf. Call it from `onComplete` in wdio.conf:
|
|
40
|
+
*
|
|
41
|
+
* import { reportingLabsComplete } from 'reporting-labs/wdio';
|
|
42
|
+
* export const config = {
|
|
43
|
+
* reporters: [[ReportingLabsWdioReporter, { outputFolder: 'reporting-labs' }]],
|
|
44
|
+
* async onComplete() { await reportingLabsComplete({ outputFolder: 'reporting-labs', title: 'My suite' }); },
|
|
45
|
+
* };
|
|
46
|
+
*/
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const template_1 = require("../template");
|
|
50
|
+
const mask_1 = require("../mask");
|
|
51
|
+
const env_1 = require("./env");
|
|
52
|
+
async function reportingLabsComplete(options = {}) {
|
|
53
|
+
const outputFolder = options.outputFolder || 'reporting-labs';
|
|
54
|
+
const partsDir = path.join(outputFolder, '.rl-wdio');
|
|
55
|
+
if (!fs.existsSync(partsDir))
|
|
56
|
+
return undefined;
|
|
57
|
+
const files = fs.readdirSync(partsDir).filter(f => f.endsWith('.json'));
|
|
58
|
+
if (!files.length)
|
|
59
|
+
return undefined;
|
|
60
|
+
const masker = (0, mask_1.makeMasker)(options.maskKeys || [], { knownValues: options.maskValues || [], fromEnv: options.maskFromEnv !== false });
|
|
61
|
+
const tests = [];
|
|
62
|
+
let startTime = Number.MAX_SAFE_INTEGER;
|
|
63
|
+
let endTime = 0;
|
|
64
|
+
let runStatus = 'passed';
|
|
65
|
+
const workers = files.length;
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
let part;
|
|
68
|
+
try {
|
|
69
|
+
part = JSON.parse(fs.readFileSync(path.join(partsDir, f), 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (typeof part.startTime === 'number')
|
|
75
|
+
startTime = Math.min(startTime, part.startTime);
|
|
76
|
+
if (typeof part.endTime === 'number')
|
|
77
|
+
endTime = Math.max(endTime, part.endTime);
|
|
78
|
+
if (part.runStatus === 'failed')
|
|
79
|
+
runStatus = 'failed';
|
|
80
|
+
for (const t of part.tests || [])
|
|
81
|
+
tests.push(t);
|
|
82
|
+
}
|
|
83
|
+
if (startTime === Number.MAX_SAFE_INTEGER)
|
|
84
|
+
startTime = Date.now();
|
|
85
|
+
if (!endTime)
|
|
86
|
+
endTime = Date.now();
|
|
87
|
+
const stats = { passed: 0, failed: 0, skipped: 0, flaky: 0, timedOut: 0, interrupted: 0, total: tests.length };
|
|
88
|
+
for (const t of tests)
|
|
89
|
+
stats[t.outcome]++;
|
|
90
|
+
const projects = [...new Set(tests.map(t => t.project).filter(Boolean))];
|
|
91
|
+
const base = process.cwd();
|
|
92
|
+
const dims = (options.dimensions ?? ['priority', 'severity', 'feature', 'owner']).map(d => d.toLowerCase());
|
|
93
|
+
const histOn = options.history?.enabled ?? true;
|
|
94
|
+
const histFile = path.resolve(base, options.history?.file ?? 'reporting-labs.history.json');
|
|
95
|
+
let entries = [];
|
|
96
|
+
if (histOn) {
|
|
97
|
+
try {
|
|
98
|
+
entries = JSON.parse(fs.readFileSync(histFile, 'utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch { /* first run */ }
|
|
101
|
+
}
|
|
102
|
+
const failedCount = stats.failed + stats.timedOut + stats.interrupted;
|
|
103
|
+
const code = (o) => o === 'passed' ? 'p' : o === 'flaky' ? 'k' : o === 'skipped' ? 's' : 'f';
|
|
104
|
+
const perTest = {};
|
|
105
|
+
for (const t of tests) {
|
|
106
|
+
const last = t.results[t.results.length - 1];
|
|
107
|
+
perTest[t.key] = [code(t.outcome), Math.round(last?.duration ?? t.duration)];
|
|
108
|
+
}
|
|
109
|
+
const current = {
|
|
110
|
+
time: startTime, duration: endTime - startTime,
|
|
111
|
+
passed: stats.passed, failed: failedCount, flaky: stats.flaky, skipped: stats.skipped, total: stats.total,
|
|
112
|
+
label: options.metadata?.build ?? (0, env_1.ciRunLabel)(process.env) ?? options.metadata?.branch, tests: perTest,
|
|
113
|
+
};
|
|
114
|
+
const history = [...entries, current].slice(-(options.history?.keep ?? 30));
|
|
115
|
+
if (histOn) {
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(histFile, JSON.stringify(history, null, 1));
|
|
118
|
+
}
|
|
119
|
+
catch { /* read-only fs */ }
|
|
120
|
+
}
|
|
121
|
+
const metadata = Object.fromEntries(Object.entries(options.metadata ?? {}).map(([k, v]) => [k, masker.maskStr(String(v))]));
|
|
122
|
+
const bdd = options.bdd ?? tests.some(t => t.results.some(r => r.steps.some(st => /^(Given|When|Then|And|But)\b/.test(st.title))));
|
|
123
|
+
const data = {
|
|
124
|
+
title: masker.maskStr(options.title ?? 'Test report'),
|
|
125
|
+
generatedAt: Date.now(),
|
|
126
|
+
startTime,
|
|
127
|
+
duration: endTime - startTime,
|
|
128
|
+
metadata,
|
|
129
|
+
projects,
|
|
130
|
+
workers,
|
|
131
|
+
stats,
|
|
132
|
+
tests,
|
|
133
|
+
history,
|
|
134
|
+
bdd,
|
|
135
|
+
rootDir: base,
|
|
136
|
+
env: (0, env_1.collectEnv)(base, Object.fromEntries(Object.entries(options.env ?? {}).map(([k, v]) => [k, masker.maskStr(String(v))])), [...new Set(projects)], workers, options.metadata?.branch).map(r => ({ ...r, v: masker.maskStr(r.v) })),
|
|
137
|
+
runStatus,
|
|
138
|
+
globalErrors: [],
|
|
139
|
+
globalOutput: [],
|
|
140
|
+
options: {
|
|
141
|
+
logo: options.logo,
|
|
142
|
+
accent: options.accent,
|
|
143
|
+
theme: options.theme ?? 'auto',
|
|
144
|
+
palette: options.palette ?? 'lab',
|
|
145
|
+
embedFonts: options.embedFonts ?? true,
|
|
146
|
+
sections: options.sections ?? [],
|
|
147
|
+
widgets: { runStrip: true, outcome: true, attention: true, dimensions: true, timeline: true, durations: true, tags: true, slowest: true, projects: true, flaky: true, environment: true, skipped: true },
|
|
148
|
+
dimensions: dims,
|
|
149
|
+
dimensionOrder: {
|
|
150
|
+
priority: ['P0', 'P1', 'P2', 'P3', 'P4'],
|
|
151
|
+
severity: ['blocker', 'critical', 'major', 'high', 'medium', 'normal', 'minor', 'low', 'trivial'],
|
|
152
|
+
...(options.dimensionOrder ?? {}),
|
|
153
|
+
},
|
|
154
|
+
project: options.project,
|
|
155
|
+
links: Object.fromEntries(Object.entries(options.links ?? {}).map(([k, v]) => [k.toLowerCase(), typeof v === 'string' ? v : v.url])),
|
|
156
|
+
customCss: options.customCss ?? '',
|
|
157
|
+
editorLinks: options.editorLinks ?? !process.env.CI,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
161
|
+
const htmlFile = path.join(outputFolder, options.outputFile ?? 'index.html');
|
|
162
|
+
fs.writeFileSync(htmlFile, (0, template_1.renderHtml)(data), 'utf8');
|
|
163
|
+
if (options.emitJson !== false)
|
|
164
|
+
fs.writeFileSync(path.join(outputFolder, options.jsonFile ?? 'report.json'), JSON.stringify(data), 'utf8');
|
|
165
|
+
// tidy the partials
|
|
166
|
+
try {
|
|
167
|
+
fs.rmSync(partsDir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
catch { /* leave them */ }
|
|
170
|
+
if (options.announce !== false) {
|
|
171
|
+
console.log(`\n reporting-labs: report written to ${path.relative(process.cwd(), htmlFile)}`);
|
|
172
|
+
}
|
|
173
|
+
if (options.pdf !== false)
|
|
174
|
+
await writePdf(htmlFile, outputFolder, options);
|
|
175
|
+
if (options.announce !== false)
|
|
176
|
+
console.log('');
|
|
177
|
+
return htmlFile;
|
|
178
|
+
}
|
|
179
|
+
/** Render the report's print layout to a PDF with the Chromium Playwright ships. Best-effort. */
|
|
180
|
+
async function writePdf(htmlFile, outputFolder, options) {
|
|
181
|
+
const name = (typeof options.pdf === 'object' && options.pdf?.file) || 'report.pdf';
|
|
182
|
+
const pdfFile = path.join(outputFolder, name);
|
|
183
|
+
try {
|
|
184
|
+
const { chromium } = await import('@playwright/test');
|
|
185
|
+
const { pathToFileURL } = await import('url');
|
|
186
|
+
const browser = await chromium.launch();
|
|
187
|
+
try {
|
|
188
|
+
const page = await browser.newPage();
|
|
189
|
+
await page.goto(pathToFileURL(htmlFile).href, { waitUntil: 'load' });
|
|
190
|
+
await page.evaluate(() => window.reportingLabsPreparePrint?.());
|
|
191
|
+
await page.pdf({ path: pdfFile, printBackground: true, preferCSSPageSize: true });
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await browser.close();
|
|
195
|
+
}
|
|
196
|
+
if (options.announce !== false)
|
|
197
|
+
console.log(` reporting-labs: PDF written to ${path.relative(process.cwd(), pdfFile)}`);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (options.announce !== false)
|
|
201
|
+
console.log(` reporting-labs: PDF skipped — ${String(e.message).split('\n')[0]}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reportingLabs for WebdriverIO.
|
|
3
|
+
*
|
|
4
|
+
* // wdio.conf.ts
|
|
5
|
+
* import ReportingLabsReporter, { reportingLabsComplete } from 'reporting-labs/wdio';
|
|
6
|
+
*
|
|
7
|
+
* export const config = {
|
|
8
|
+
* reporters: [[ReportingLabsReporter, { outputFolder: 'reporting-labs' }]],
|
|
9
|
+
* async onComplete() { await reportingLabsComplete({ outputFolder: 'reporting-labs', title: 'My suite' }); },
|
|
10
|
+
* };
|
|
11
|
+
*/
|
|
12
|
+
import ReportingLabsWdioReporter from './reporter';
|
|
13
|
+
export { ReportingLabsWdioReporter };
|
|
14
|
+
export type { ReportingLabsWdioOptions } from './reporter';
|
|
15
|
+
export { reportingLabsComplete } from './finalize';
|
|
16
|
+
export type { ReportingLabsCompleteOptions } from './finalize';
|
|
17
|
+
export default ReportingLabsWdioReporter;
|