reporting-labs 0.6.0 → 0.6.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 +156 -20
- package/bin/build-template.js +101 -0
- package/dist/template.html +1659 -0
- package/dist/template.js +217 -2
- package/package.json +2 -2
package/dist/template.js
CHANGED
|
@@ -660,6 +660,12 @@ a.btn{text-decoration:none}
|
|
|
660
660
|
.skp{list-style:none;margin:0;padding:0} .skp li{border-top:1px solid var(--line)} .skp li:first-child{border-top:0}
|
|
661
661
|
.skp button{width:100%;text-align:left;padding:8px 4px;display:flex;flex-direction:column;gap:2px}
|
|
662
662
|
.skp .t{font-size:13px;font-weight:500;color:var(--ink)} .skp .r{font-size:12px;color:var(--ink-3)}
|
|
663
|
+
.gchart .gchart-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
|
664
|
+
.gchart .gchart-head h2{margin:0}
|
|
665
|
+
.gchart .gchart-sub{color:var(--ink-3);font-size:12px;margin-top:4px}
|
|
666
|
+
.gchart .gchart-body{margin-top:14px}
|
|
667
|
+
.gchart .empty{padding:22px 0;color:var(--ink-3);text-align:center}
|
|
668
|
+
.graphs-grid .card{background:var(--surface)}
|
|
663
669
|
.kv.env{grid-template-columns:max-content 1fr;gap:8px 16px} .kv.env dt{font-size:11.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding-top:1px} .kv.env dd{font:12.5px var(--mono);word-break:break-word} .kv.env a{color:var(--accent-2)}
|
|
664
670
|
.hm-wrap{overflow-x:auto} .hm{border-collapse:separate;border-spacing:4px;font-size:12.5px;min-width:100%}
|
|
665
671
|
.hm th{font-weight:600;color:var(--ink-2);text-align:left;padding:4px 8px;white-space:nowrap} .hm thead th{font:600 11px var(--mono);color:var(--ink-3);text-align:center;letter-spacing:.03em}
|
|
@@ -740,6 +746,7 @@ app.append(h('div',{class:'band'}, stripe(), header(), nav()),
|
|
|
740
746
|
h('div',{class:'view','data-view':'tests'}, main()),
|
|
741
747
|
h('div',{class:'view','data-view':'failures'}, h('div',{class:'view-pad'}, failuresView())),
|
|
742
748
|
h('div',{class:'view','data-view':'api'}, h('div',{class:'view-pad'}, apiView())),
|
|
749
|
+
h('div',{class:'view','data-view':'graphs'}, graphsView()),
|
|
743
750
|
hasTimelineView()? h('div',{class:'view','data-view':'timeline'}, h('div',{class:'view-pad'}, timelineView())) : null);
|
|
744
751
|
select(state.selected || firstInteresting(), true);
|
|
745
752
|
showView(state.view);
|
|
@@ -747,7 +754,7 @@ window.addEventListener('keydown', e=>{
|
|
|
747
754
|
if(e.key==='Escape'){const lb=$('.lb'); if(lb) lb.remove(); return;}
|
|
748
755
|
const tag=(e.target.tagName||'').toLowerCase(); if(tag==='input'||tag==='select'||tag==='textarea') return;
|
|
749
756
|
if(e.key==='/'){ e.preventDefault(); showView('tests'); $('.tools input').focus(); return; }
|
|
750
|
-
const vk={'1':'overview','2':'tests','3':'failures','4':'api','5':'timeline'}[e.key]; if(vk&&document.querySelector('.nav button[data-view='+vk+']')){ showView(vk); return; }
|
|
757
|
+
const vk={'1':'overview','2':'tests','3':'failures','4':'api','5':'graphs','6':'timeline'}[e.key]; if(vk&&document.querySelector('.nav button[data-view='+vk+']')){ showView(vk); return; }
|
|
751
758
|
if(e.key==='f'){ state.status=state.status==='failed'?'all':'failed'; refresh(); return; }
|
|
752
759
|
if(e.key==='j'||e.key==='k'){ const ids=[...document.querySelectorAll('.item')].map(i=>i.dataset.id); if(!ids.length) return; let i=ids.indexOf(state.selected); i=e.key==='j'?Math.min(ids.length-1,i+1):Math.max(0,i-1); select(ids[i]); }
|
|
753
760
|
});
|
|
@@ -762,7 +769,7 @@ function showView(v){
|
|
|
762
769
|
}
|
|
763
770
|
function nav(){
|
|
764
771
|
const s=data.stats, f=s.failed+s.timedOut+s.interrupted, apiN=data.tests.reduce((a,t)=>a+t.results.reduce((b,r)=>b+r.api.length,0),0);
|
|
765
|
-
const tabs=[['overview','Overview',null],['tests','Tests',s.total],['failures','Failures',f+s.flaky,f>0],['api','API',apiN],['timeline','Timeline',null]].filter(t=>(t[0]!=='api'||apiN>0)&&(t[0]!=='timeline'||hasTimelineView()));
|
|
772
|
+
const tabs=[['overview','Overview',null],['tests','Tests',s.total],['failures','Failures',f+s.flaky,f>0],['api','API',apiN],['graphs','Graphs',null],['timeline','Timeline',null]].filter(t=>(t[0]!=='api'||apiN>0)&&(t[0]!=='timeline'||hasTimelineView())&&(t[0]!=='graphs'||data.tests.length>0));
|
|
766
773
|
return h('nav',{class:'nav'}, h('div',{}, tabs.map(([v,l,n,bad])=>h('button',{'data-view':v,'aria-selected':state.view===v,onclick:()=>showView(v)}, l, n!=null?h('span',{class:'cnt'+(bad?' bad':'')},n):null))));
|
|
767
774
|
}
|
|
768
775
|
function failuresView(){
|
|
@@ -802,6 +809,214 @@ function timelineView(){
|
|
|
802
809
|
return h('div',{class:'grid'}, cards);
|
|
803
810
|
}
|
|
804
811
|
function hasTimelineView(){ return data.options.widgets.timeline!==false || data.options.widgets.durations!==false; }
|
|
812
|
+
|
|
813
|
+
/* ---------- Graphs tab: 4 presentation-ready charts with PNG download ---------- */
|
|
814
|
+
function gEsc(s){ return String(s==null?'':s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
815
|
+
function graphsView(){
|
|
816
|
+
return h('div',{class:'view-pad'},
|
|
817
|
+
h('div',{class:'grid graphs-grid'},
|
|
818
|
+
chartCard('outcomes-by-priority', 'Outcomes by priority', 'Stacked pass / fail / flaky / skipped for each priority tier.', chartPriorityOutcome),
|
|
819
|
+
chartCard('failure-categories', 'Failure categories', 'What kinds of failures are hurting the suite.', chartFailureCategories),
|
|
820
|
+
chartCard('duration-distribution','Duration distribution', 'How test durations are spread across the run.', chartDurationHistogram),
|
|
821
|
+
chartCard('trend-by-priority', 'Pass rate trend by priority', 'How each priority tier trends over the last runs.', chartTrendByPriority),
|
|
822
|
+
chartCard('owner-leaderboard', 'Owner leaderboard', 'Failures and flakes ranked by owner. Add meta({owner:...}) to see this.', chartOwnerLeaderboard),
|
|
823
|
+
));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/* Chart 5: horizontal stacked bar per owner — total tests, failed vs flaky highlighted */
|
|
827
|
+
function chartOwnerLeaderboard(){
|
|
828
|
+
const rows = new Map();
|
|
829
|
+
for(const t of data.tests){
|
|
830
|
+
const o = (t.meta.owner||'').toString().trim(); if(!o) continue;
|
|
831
|
+
let r = rows.get(o); if(!r){ r={owner:o, total:0, failed:0, flaky:0, passed:0, skipped:0}; rows.set(o,r); }
|
|
832
|
+
r.total++;
|
|
833
|
+
if(isFail(t.outcome)) r.failed++;
|
|
834
|
+
else if(t.outcome==='flaky') r.flaky++;
|
|
835
|
+
else if(t.outcome==='skipped') r.skipped++;
|
|
836
|
+
else if(t.outcome==='passed') r.passed++;
|
|
837
|
+
}
|
|
838
|
+
const list = [...rows.values()].sort((a,b)=> (b.failed - a.failed) || (b.flaky - a.flaky) || (b.total - a.total));
|
|
839
|
+
if(!list.length) return null;
|
|
840
|
+
const maxTotal = Math.max(...list.map(r=>r.total));
|
|
841
|
+
const W=520, rowH=28, gap=8, labW=110, barW=W-labW-70;
|
|
842
|
+
const H = list.length*(rowH+gap)+30;
|
|
843
|
+
const parts = list.map((r,i)=>{
|
|
844
|
+
const y=i*(rowH+gap)+14, scale=barW/maxTotal;
|
|
845
|
+
const passedW = r.passed*scale, flakyW = r.flaky*scale, failedW = r.failed*scale, skippedW = r.skipped*scale;
|
|
846
|
+
let x = labW;
|
|
847
|
+
const segs=[];
|
|
848
|
+
if(passedW>0){ segs.push('<rect x="'+x+'" y="'+y+'" width="'+passedW+'" height="'+rowH+'" fill="var(--pass)" rx="3"><title>'+gEsc(r.owner)+' · '+r.passed+' passed</title></rect>'); x+=passedW; }
|
|
849
|
+
if(flakyW>0){ segs.push('<rect x="'+x+'" y="'+y+'" width="'+flakyW+'" height="'+rowH+'" fill="var(--flaky)"><title>'+gEsc(r.owner)+' · '+r.flaky+' flaky</title></rect>'); x+=flakyW; }
|
|
850
|
+
if(failedW>0){ segs.push('<rect x="'+x+'" y="'+y+'" width="'+failedW+'" height="'+rowH+'" fill="var(--fail)"><title>'+gEsc(r.owner)+' · '+r.failed+' failed</title></rect>'); x+=failedW; }
|
|
851
|
+
if(skippedW>0){segs.push('<rect x="'+x+'" y="'+y+'" width="'+skippedW+'" height="'+rowH+'" fill="var(--skip)"><title>'+gEsc(r.owner)+' · '+r.skipped+' skipped</title></rect>'); x+=skippedW; }
|
|
852
|
+
return '<text x="0" y="'+(y+rowH/2+4)+'" fill="var(--ink)" font-size="12" font-weight="600">'+gEsc(r.owner)+'</text>'+segs.join('')+
|
|
853
|
+
'<text x="'+(labW+barW+8)+'" y="'+(y+rowH/2+4)+'" fill="var(--ink-2)" font-size="12" font-variant-numeric="tabular-nums">'+r.total+'</text>';
|
|
854
|
+
}).join('');
|
|
855
|
+
const legendPairs = [['Passed','var(--pass)'],['Flaky','var(--flaky)'],['Failed','var(--fail)'],['Skipped','var(--skip)']];
|
|
856
|
+
const hasKind = { Passed:list.some(r=>r.passed>0), Flaky:list.some(r=>r.flaky>0), Failed:list.some(r=>r.failed>0), Skipped:list.some(r=>r.skipped>0) };
|
|
857
|
+
const legend = legendPairs.filter(([n])=>hasKind[n]).map(([n,c],i)=>'<g transform="translate('+(labW+i*90)+','+(H-4)+')"><rect width="10" height="10" y="-9" fill="'+c+'"/><text x="14" y="0" font-size="11" fill="var(--ink-2)">'+n+'</text></g>').join('');
|
|
858
|
+
return h('div',{class:'gchart-body',html:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+(H+14)+'" width="100%">'+parts+legend+'</svg>'});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function chartCard(id, title, sub, chartFn){
|
|
862
|
+
const chart = chartFn();
|
|
863
|
+
const empty = !chart;
|
|
864
|
+
return h('div',{class:'card w6 gchart','data-chart':id},
|
|
865
|
+
h('div',{class:'gchart-head'},
|
|
866
|
+
h('div',{},
|
|
867
|
+
h('h2',{},title),
|
|
868
|
+
h('div',{class:'gchart-sub'},sub)),
|
|
869
|
+
empty? null : h('button',{class:'btn',title:'Download this chart as a PNG image',
|
|
870
|
+
onclick:e=>downloadChartPng(id, title, e.currentTarget)},'Download PNG')),
|
|
871
|
+
empty? h('div',{class:'empty'},'Not enough data yet. Add meta({ priority }) or run the suite once more.') : chart);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* Chart 1: horizontal stacked bar per priority (P0..P4 in order, plus 'no priority') */
|
|
875
|
+
function chartPriorityOutcome(){
|
|
876
|
+
const order = ['P0','P1','P2','P3','P4'];
|
|
877
|
+
const outs = ['passed','flaky','failed','timedOut','interrupted','skipped'];
|
|
878
|
+
const colors= {passed:'var(--pass)', flaky:'var(--flaky)', failed:'var(--fail)', timedOut:'var(--fail)', interrupted:'var(--fail)', skipped:'var(--skip)'};
|
|
879
|
+
const buckets = new Map();
|
|
880
|
+
const put=(k,o)=>{ let m=buckets.get(k); if(!m){ m={}; for(const x of outs) m[x]=0; buckets.set(k,m); } m[o]=(m[o]||0)+1; };
|
|
881
|
+
for(const t of data.tests){ const p=(t.meta.priority||'').toString().trim().toUpperCase(); put(order.includes(p)? p : 'No priority', t.outcome); }
|
|
882
|
+
const rows = [...order.filter(k=>buckets.has(k)), ...(buckets.has('No priority')?['No priority']:[])];
|
|
883
|
+
if(!rows.length) return null;
|
|
884
|
+
const maxN = Math.max(...rows.map(r=>outs.reduce((a,o)=>a+buckets.get(r)[o],0)));
|
|
885
|
+
const W=520, rowH=34, gap=10, labW=90, barW=W-labW-70;
|
|
886
|
+
const H = rows.length*(rowH+gap)+30;
|
|
887
|
+
const parts = rows.map((r,i)=>{
|
|
888
|
+
const y=i*(rowH+gap)+18, m=buckets.get(r), tot=outs.reduce((a,o)=>a+m[o],0), scale=barW/maxN;
|
|
889
|
+
let x=labW;
|
|
890
|
+
const segs=[]; for(const o of outs){ const w=m[o]*scale; if(w<=0) continue; segs.push('<rect x="'+x+'" y="'+y+'" width="'+w+'" height="'+rowH+'" fill="'+colors[o]+'" data-outcome="'+o+'"><title>'+r+' · '+label[o]+' '+m[o]+'</title></rect>'); x+=w; }
|
|
891
|
+
return '<text x="0" y="'+(y+rowH/2+4)+'" fill="var(--ink)" font-size="12" font-weight="600">'+r+'</text>'+
|
|
892
|
+
segs.join('')+
|
|
893
|
+
'<text x="'+(labW+barW+8)+'" y="'+(y+rowH/2+4)+'" fill="var(--ink-2)" font-size="12" font-variant-numeric="tabular-nums">'+tot+'</text>';
|
|
894
|
+
}).join('');
|
|
895
|
+
const legend = outs.filter(o=>rows.some(r=>buckets.get(r)[o]>0)).map(o=>'<g><rect width="10" height="10" fill="'+colors[o]+'"/><text x="14" y="9" fill="var(--ink-2)" font-size="11">'+label[o]+'</text></g>');
|
|
896
|
+
const legendSvg = '<g transform="translate('+labW+','+(H-2)+')">'+legend.map((g,i)=>'<g transform="translate('+(i*90)+',0)">'+g.slice(3,-4)+'</g>').join('')+'</g>';
|
|
897
|
+
return h('div',{class:'gchart-body',html:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+(H+18)+'" width="100%">'+parts+legendSvg+'</svg>'});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/* Chart 2: horizontal bar of the 18 error 'kinds' from explainError, sorted by count desc */
|
|
901
|
+
function chartFailureCategories(){
|
|
902
|
+
const counts = new Map();
|
|
903
|
+
for(const t of data.tests){ const r=t.results[t.results.length-1], e=r&&r.errors[0]; if(!e||!e.explain) continue; const k=e.explain.label||e.explain.kind||'other'; counts.set(k,(counts.get(k)||0)+1); }
|
|
904
|
+
const rows=[...counts.entries()].sort((a,b)=>b[1]-a[1]);
|
|
905
|
+
if(!rows.length) return null;
|
|
906
|
+
const maxN = rows[0][1];
|
|
907
|
+
const W=520, rowH=22, gap=6, labW=200, barW=W-labW-40;
|
|
908
|
+
const H = rows.length*(rowH+gap)+8;
|
|
909
|
+
const parts = rows.map(([k,n],i)=>{
|
|
910
|
+
const y=i*(rowH+gap)+8, w=Math.max(2, n/maxN*barW);
|
|
911
|
+
return '<text x="0" y="'+(y+rowH/2+4)+'" fill="var(--ink)" font-size="12">'+gEsc(k)+'</text>'+
|
|
912
|
+
'<rect x="'+labW+'" y="'+y+'" width="'+w+'" height="'+rowH+'" fill="var(--fail)" rx="3"><title>'+gEsc(k)+' · '+n+'</title></rect>'+
|
|
913
|
+
'<text x="'+(labW+w+6)+'" y="'+(y+rowH/2+4)+'" fill="var(--ink-2)" font-size="12" font-variant-numeric="tabular-nums">'+n+'</text>';
|
|
914
|
+
}).join('');
|
|
915
|
+
return h('div',{class:'gchart-body',html:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+H+'" width="100%">'+parts+'</svg>'});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Chart 3: duration histogram — bars for 5 buckets across all tests */
|
|
919
|
+
function chartDurationHistogram(){
|
|
920
|
+
if(!data.tests.length) return null;
|
|
921
|
+
const buckets = [
|
|
922
|
+
{label:'< 1s', min:0, max:1000},
|
|
923
|
+
{label:'1-3s', min:1000, max:3000},
|
|
924
|
+
{label:'3-10s', min:3000, max:10000},
|
|
925
|
+
{label:'10-30s', min:10000,max:30000},
|
|
926
|
+
{label:'30s+', min:30000,max:Infinity},
|
|
927
|
+
];
|
|
928
|
+
const counts = buckets.map(()=>0);
|
|
929
|
+
for(const t of data.tests){ const d=t.duration||0; for(let i=0;i<buckets.length;i++){ if(d>=buckets[i].min && d<buckets[i].max){ counts[i]++; break; } } }
|
|
930
|
+
const maxN = Math.max(1,...counts);
|
|
931
|
+
const W=520, colW=80, gap=16, padL=32, padT=24, padB=36, chartH=180;
|
|
932
|
+
const H = chartH+padT+padB;
|
|
933
|
+
const bars = counts.map((n,i)=>{
|
|
934
|
+
const x=padL+i*(colW+gap), h=n/maxN*chartH, y=padT+chartH-h;
|
|
935
|
+
return '<rect x="'+x+'" y="'+y+'" width="'+colW+'" height="'+h+'" fill="var(--accent)" rx="3"><title>'+buckets[i].label+' · '+n+' tests</title></rect>'+
|
|
936
|
+
(n>0?'<text x="'+(x+colW/2)+'" y="'+(y-4)+'" text-anchor="middle" fill="var(--ink)" font-size="12" font-weight="600" font-variant-numeric="tabular-nums">'+n+'</text>':'')+
|
|
937
|
+
'<text x="'+(x+colW/2)+'" y="'+(padT+chartH+22)+'" text-anchor="middle" fill="var(--ink-2)" font-size="12">'+buckets[i].label+'</text>';
|
|
938
|
+
}).join('');
|
|
939
|
+
return h('div',{class:'gchart-body',html:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+H+'" width="100%">'+bars+'</svg>'});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/* Chart 4: multi-line — pass rate per priority over the last N history entries */
|
|
943
|
+
function chartTrendByPriority(){
|
|
944
|
+
const hist = data.history || [];
|
|
945
|
+
if(hist.length < 2) return null;
|
|
946
|
+
const orders = ['P0','P1','P2','P3','P4'];
|
|
947
|
+
const seenPri = new Set();
|
|
948
|
+
for(const t of data.tests){ const p=(t.meta.priority||'').toString().trim().toUpperCase(); if(orders.includes(p)) seenPri.add(p); }
|
|
949
|
+
const priorities = orders.filter(p=>seenPri.has(p));
|
|
950
|
+
if(!priorities.length) return null;
|
|
951
|
+
const colors = { P0:'#E5405E', P1:'#F59E0B', P2:'#3B82F6', P3:'#8B95A5', P4:'#6B7280' };
|
|
952
|
+
const currentTests = new Map(data.tests.map(t=>[t.key, (t.meta.priority||'').toString().trim().toUpperCase()]));
|
|
953
|
+
const series = priorities.map(pri=>{
|
|
954
|
+
const points = hist.map(e=>{
|
|
955
|
+
let p=0,f=0; for(const [key,val] of Object.entries(e.tests||{})){ if(currentTests.get(key)!==pri) continue; const outcome=val[0]||''; if(outcome==='p') p++; else if(outcome==='f'||outcome==='t'||outcome==='i') f++; }
|
|
956
|
+
const ran = p+f; return ran? Math.round(p/ran*100) : null;
|
|
957
|
+
});
|
|
958
|
+
return { pri, color: colors[pri]||'#888', points };
|
|
959
|
+
}).filter(sr=>sr.points.some(v=>v!==null));
|
|
960
|
+
if(!series.length) return null;
|
|
961
|
+
const W=520, padL=36, padR=24, padT=16, padB=28, chartH=180;
|
|
962
|
+
const H = chartH+padT+padB;
|
|
963
|
+
const N = hist.length;
|
|
964
|
+
const xAt = i => padL + (N===1? 0 : i*((W-padL-padR)/(N-1)));
|
|
965
|
+
const yAt = pct => padT + (100-pct)/100*chartH;
|
|
966
|
+
const grid = [0,25,50,75,100].map(y=>'<line x1="'+padL+'" x2="'+(W-padR)+'" y1="'+yAt(y)+'" y2="'+yAt(y)+'" stroke="var(--line)" stroke-dasharray="2 3"/><text x="'+(padL-6)+'" y="'+(yAt(y)+3)+'" text-anchor="end" font-size="10" fill="var(--ink-3)">'+y+'%</text>').join('');
|
|
967
|
+
const lines = series.map(sr=>{
|
|
968
|
+
// A series may start with (or contain) nulls — runs where this priority
|
|
969
|
+
// had no tests. Each stretch after a gap must restart with M, or the
|
|
970
|
+
// path begins with L and the browser rejects it.
|
|
971
|
+
let pen=false;
|
|
972
|
+
const d = sr.points.map((v,i)=>{ if(v===null){ pen=false; return null; } const cmd=pen?'L':'M'; pen=true; return cmd+xAt(i)+' '+yAt(v); }).filter(Boolean).join(' ');
|
|
973
|
+
const dots = sr.points.map((v,i)=> v===null? '' : '<circle cx="'+xAt(i)+'" cy="'+yAt(v)+'" r="2.5" fill="'+sr.color+'"><title>'+sr.pri+' · '+v+'% at run '+(i+1)+'</title></circle>').join('');
|
|
974
|
+
return '<path d="'+d+'" fill="none" stroke="'+sr.color+'" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>'+dots;
|
|
975
|
+
}).join('');
|
|
976
|
+
const legend = series.map((sr,i)=>'<g transform="translate('+(padL+i*70)+','+(H-8)+')"><rect width="10" height="10" y="-9" fill="'+sr.color+'"/><text x="14" y="0" font-size="11" fill="var(--ink-2)">'+sr.pri+'</text></g>').join('');
|
|
977
|
+
return h('div',{class:'gchart-body',html:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+H+'" width="100%">'+grid+lines+legend+'</svg>'});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/* PNG download: serialize the chart's SVG, draw onto a canvas at 2x, trigger download. */
|
|
981
|
+
function downloadChartPng(chartId, title, btn){
|
|
982
|
+
const card = document.querySelector('[data-chart="'+chartId+'"]');
|
|
983
|
+
const src = card && card.querySelector('svg');
|
|
984
|
+
if(!src) return;
|
|
985
|
+
const label = btn.textContent; btn.textContent = 'Rendering…'; btn.disabled = true;
|
|
986
|
+
const clone = src.cloneNode(true);
|
|
987
|
+
clone.setAttribute('xmlns','http://www.w3.org/2000/svg');
|
|
988
|
+
// Inline computed colors so CSS var() references resolve inside the standalone SVG.
|
|
989
|
+
const cs = getComputedStyle(document.documentElement);
|
|
990
|
+
const varRe = /var\(--([a-z0-9-]+)\)/gi;
|
|
991
|
+
const walk = el => { for(const a of ['fill','stroke']){ const v=el.getAttribute(a); if(v && varRe.test(v)){ el.setAttribute(a, v.replace(varRe,(_,n)=>cs.getPropertyValue('--'+n).trim() || 'currentColor')); } } for(const c of el.children) walk(c); };
|
|
992
|
+
walk(clone);
|
|
993
|
+
// Ensure the SVG has explicit width/height (in px) for correct raster size.
|
|
994
|
+
const vb = clone.viewBox && clone.viewBox.baseVal;
|
|
995
|
+
const w = (vb && vb.width) || 800, hpx = (vb && vb.height) || 400;
|
|
996
|
+
const scale = 2;
|
|
997
|
+
clone.setAttribute('width', w); clone.setAttribute('height', hpx);
|
|
998
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
999
|
+
const svgBlob = new Blob([xml], {type:'image/svg+xml;charset=utf-8'});
|
|
1000
|
+
const url = URL.createObjectURL(svgBlob);
|
|
1001
|
+
const img = new Image();
|
|
1002
|
+
img.onload = () => {
|
|
1003
|
+
const canvas = document.createElement('canvas');
|
|
1004
|
+
canvas.width = w*scale; canvas.height = hpx*scale;
|
|
1005
|
+
const ctx = canvas.getContext('2d');
|
|
1006
|
+
ctx.fillStyle = cs.getPropertyValue('--surface').trim() || '#ffffff';
|
|
1007
|
+
ctx.fillRect(0,0,canvas.width,canvas.height);
|
|
1008
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
1009
|
+
URL.revokeObjectURL(url);
|
|
1010
|
+
canvas.toBlob(b => {
|
|
1011
|
+
const a = document.createElement('a');
|
|
1012
|
+
a.href = URL.createObjectURL(b); a.download = 'reporting-labs-'+chartId+'.png'; a.click();
|
|
1013
|
+
URL.revokeObjectURL(a.href);
|
|
1014
|
+
btn.textContent = label; btn.disabled = false;
|
|
1015
|
+
}, 'image/png');
|
|
1016
|
+
};
|
|
1017
|
+
img.onerror = () => { btn.textContent = 'PNG failed'; btn.disabled = false; URL.revokeObjectURL(url); };
|
|
1018
|
+
img.src = url;
|
|
1019
|
+
}
|
|
805
1020
|
function stripe(){
|
|
806
1021
|
const s=data.stats, f=s.failed+s.timedOut+s.interrupted, tot=s.total||1;
|
|
807
1022
|
return h('div',{class:'stripe','aria-hidden':'true'}, [['passed',s.passed],['flaky',s.flaky],['failed',f],['skipped',s.skipped]].filter(x=>x[1]).map(([k,n])=>h('i',{style:'width:'+(n/tot*100)+'%;background:'+colorOf(k)})));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"LICENSE"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"build": "tsc -p tsconfig.json && node bin/build-template.js",
|
|
47
47
|
"demo": "npm run build && cd example && npx playwright test; cd .. && echo 'open example/reporting-labs/index.html'",
|
|
48
48
|
"prepublishOnly": "npm run build",
|
|
49
49
|
"pack:check": "npm run build && npm pack --dry-run"
|