reporting-labs 0.2.0 → 0.2.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 +2 -2
- package/bin/config-template.txt +2 -1
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +19 -1
- package/dist/template.js +13 -6
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ Every option is optional. `npx reporting-labs init` writes them all, with commen
|
|
|
224
224
|
| Option | Default | What it does |
|
|
225
225
|
|---|---|---|
|
|
226
226
|
| `title` | `'Test report'` | Title in the header |
|
|
227
|
-
| `logo` | – |
|
|
227
|
+
| `logo` | – | Your logo next to the title: `'logo.png'` (a file next to the config, embedded in the report) or an https URL |
|
|
228
228
|
| `project` | – | `{ name, version, team, url }` shown under the title |
|
|
229
229
|
| `metadata` | `{}` | Chips in the header, e.g. `{ env: 'staging', build: '#1842' }`. `build` labels the run in the trend; in CI the run number is used when it is not set |
|
|
230
230
|
| `env` | – | Extra rows on the Environment card |
|
|
@@ -232,7 +232,7 @@ Every option is optional. `npx reporting-labs init` writes them all, with commen
|
|
|
232
232
|
| `maskKeys` | `[]` | Extra keys to mask as `****` |
|
|
233
233
|
| `dimensions` | `['priority','severity','feature','owner']` | Meta keys that get charts and filters |
|
|
234
234
|
| `dimensionOrder` | P0…P4, blocker…trivial | Sort order per dimension |
|
|
235
|
-
| `widgets` | all on | Hide cards: `{ tags: false, timeline: false, ... }
|
|
235
|
+
| `widgets` | all on | Hide cards: `{ tags: false, timeline: false, ... }`. Failure clusters and the Trend chart are always shown |
|
|
236
236
|
| `sections` | `[]` | Extra HTML below the summary, e.g. release notes |
|
|
237
237
|
| `history` | `{ enabled: true, keep: 30 }` | Run history file; `file` sets a custom path |
|
|
238
238
|
| `palette` | `'lab'` | `'lab'` (blue), `'ocean'`, `'ember'`, `'mono'` |
|
package/bin/config-template.txt
CHANGED
|
@@ -9,7 +9,7 @@ const config: ReportingLabsOptions = {
|
|
|
9
9
|
|
|
10
10
|
// ── Look ─────────────────────────────────────────────────────────────────────
|
|
11
11
|
title: 'My app – regression', // shown in the header
|
|
12
|
-
// logo: '
|
|
12
|
+
// logo: 'logo.png', // your logo next to the title: a file next to this config (embedded), or an https URL
|
|
13
13
|
// palette: 'lab', // 'lab' (blue, default) | 'ocean' | 'ember' | 'mono'; viewers can switch
|
|
14
14
|
// accent: '#7C3AED', // your brand color instead of the palette accent
|
|
15
15
|
// theme: 'auto', // 'auto' (follows OS, default) | 'light' | 'dark'
|
|
@@ -54,6 +54,7 @@ const config: ReportingLabsOptions = {
|
|
|
54
54
|
// },
|
|
55
55
|
|
|
56
56
|
// ── Widgets on the overview (hide what you do not need) ──────────────────────
|
|
57
|
+
// Failure clusters and the Trend chart are always shown.
|
|
57
58
|
// widgets: {
|
|
58
59
|
// runStrip: true, // pass/fail strip under the header
|
|
59
60
|
// outcome: true, // outcome ring
|
package/dist/reporter.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export default class ReportingLabsReporter implements Reporter {
|
|
|
31
31
|
private metaKeys;
|
|
32
32
|
/** Pull meta values (priority, owner, story, epic...) from annotations and tags. */
|
|
33
33
|
private extractMeta;
|
|
34
|
+
/** A local image file (path relative to the config) is embedded as a data URI so the report stays self-contained. URLs and data URIs pass through. */
|
|
35
|
+
private resolveLogo;
|
|
34
36
|
private rel;
|
|
35
37
|
private serializeError;
|
|
36
38
|
private outcome;
|
package/dist/reporter.js
CHANGED
|
@@ -167,7 +167,7 @@ class ReportingLabsReporter {
|
|
|
167
167
|
globalOutput: this.globalOutput,
|
|
168
168
|
shard: this.config.shard ? { current: this.config.shard.current, total: this.config.shard.total } : undefined,
|
|
169
169
|
options: {
|
|
170
|
-
logo: this.options.logo,
|
|
170
|
+
logo: this.resolveLogo(this.options.logo),
|
|
171
171
|
accent: this.options.accent,
|
|
172
172
|
theme: this.options.theme ?? 'auto',
|
|
173
173
|
palette: this.options.palette ?? 'lab',
|
|
@@ -329,6 +329,24 @@ class ReportingLabsReporter {
|
|
|
329
329
|
}
|
|
330
330
|
return meta;
|
|
331
331
|
}
|
|
332
|
+
/** A local image file (path relative to the config) is embedded as a data URI so the report stays self-contained. URLs and data URIs pass through. */
|
|
333
|
+
resolveLogo(logo) {
|
|
334
|
+
if (!logo || /^(https?:|data:)/i.test(logo))
|
|
335
|
+
return logo;
|
|
336
|
+
const base = this.config.configFile ? path.dirname(this.config.configFile) : this.config.rootDir;
|
|
337
|
+
const file = path.resolve(base, logo);
|
|
338
|
+
if (!fs.existsSync(file)) {
|
|
339
|
+
console.warn(`reporting-labs: logo not found at ${file}`);
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
const mime = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon' };
|
|
343
|
+
const type = mime[path.extname(file).toLowerCase()];
|
|
344
|
+
if (!type) {
|
|
345
|
+
console.warn(`reporting-labs: logo ${logo} is not a png, jpg, svg, gif or webp file`);
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
return `data:${type};base64,${fs.readFileSync(file).toString('base64')}`;
|
|
349
|
+
}
|
|
332
350
|
rel(file) {
|
|
333
351
|
const base = this.config.configFile ? path.dirname(this.config.configFile) : this.config.rootDir;
|
|
334
352
|
return path.relative(base, file).split(path.sep).join('/');
|
package/dist/template.js
CHANGED
|
@@ -473,6 +473,7 @@ html,body{-webkit-font-smoothing:antialiased}
|
|
|
473
473
|
.hdr h1,.hdr .proj-blk h1{color:#fff;font-size:20px;font-weight:600;letter-spacing:-.01em;line-height:1.2}
|
|
474
474
|
.hdr .proj-blk .sub,.hdr .proj-blk .sub a,.hdr .when{color:rgba(255,255,255,.78)}
|
|
475
475
|
.hdr .logo{width:42px;height:42px;border-radius:11px;flex:none;box-shadow:0 0 0 1px rgba(255,255,255,.35),0 6px 16px -6px rgba(0,0,0,.45)} .hdr .logo svg{width:42px;height:42px;display:block;border-radius:11px}
|
|
476
|
+
.hdr img.logo.brand{width:auto;max-width:180px;height:42px;object-fit:contain;border-radius:8px;background:#fff;padding:4px;box-sizing:border-box}
|
|
476
477
|
.hdr img{height:32px;border-radius:6px}
|
|
477
478
|
.hdr .chip{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.22);color:rgba(255,255,255,.85);border-radius:999px;padding:4px 9px}
|
|
478
479
|
.hdr .chip b{color:#fff}
|
|
@@ -725,7 +726,7 @@ app.append(h('div',{class:'band'}, stripe(), header(), nav()),
|
|
|
725
726
|
h('div',{class:'view','data-view':'tests'}, main()),
|
|
726
727
|
h('div',{class:'view','data-view':'failures'}, h('div',{class:'view-pad'}, failuresView())),
|
|
727
728
|
h('div',{class:'view','data-view':'api'}, h('div',{class:'view-pad'}, apiView())),
|
|
728
|
-
h('div',{class:'view','data-view':'timeline'}, h('div',{class:'view-pad'}, timelineView())));
|
|
729
|
+
hasTimelineView()? h('div',{class:'view','data-view':'timeline'}, h('div',{class:'view-pad'}, timelineView())) : null);
|
|
729
730
|
select(state.selected || firstInteresting(), true);
|
|
730
731
|
showView(state.view);
|
|
731
732
|
window.addEventListener('keydown', e=>{
|
|
@@ -740,13 +741,14 @@ window.addEventListener('keydown', e=>{
|
|
|
740
741
|
/* ---------- header ---------- */
|
|
741
742
|
function setHash(){ const p=new URLSearchParams(); if(state.view!=='overview') p.set('view',state.view); if(state.selected&&state.view==='tests') p.set('t',state.selected); const hs=p.toString(); history.replaceState(null,'',hs?'#'+hs:location.pathname+location.search); }
|
|
742
743
|
function showView(v){
|
|
744
|
+
if(!document.querySelector('.view[data-view='+v+']')) v='overview';
|
|
743
745
|
state.view=v; document.querySelectorAll('.view').forEach(el=>el.classList.toggle('on',el.dataset.view===v));
|
|
744
746
|
document.querySelectorAll('.nav button[data-view]').forEach(b=>b.setAttribute('aria-selected',b.dataset.view===v));
|
|
745
747
|
setHash(); try{window.scrollTo(0,0);}catch(e){}
|
|
746
748
|
}
|
|
747
749
|
function nav(){
|
|
748
750
|
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);
|
|
749
|
-
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);
|
|
751
|
+
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()));
|
|
750
752
|
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))));
|
|
751
753
|
}
|
|
752
754
|
function failuresView(){
|
|
@@ -780,8 +782,12 @@ function apiView(){
|
|
|
780
782
|
h('td',{class:'u',title:c.url},c.url), h('td',{}, h('span',{class:'sc '+cls,style:'font:12px var(--mono);font-weight:600;color:'+(cls==='bad'?'var(--fail)':cls==='warn'?'var(--flaky-ink)':'var(--pass)')},st||'—')), h('td',{class:'t'},c.duration!=null?ms(c.duration):'—'), h('td',{class:'t',title:t.title},t.title)); })))));
|
|
781
783
|
}
|
|
782
784
|
function timelineView(){
|
|
783
|
-
|
|
785
|
+
const W=data.options.widgets, cards=[];
|
|
786
|
+
if(W.timeline!==false) cards.push(h('div',{class:'card w12'}, h('h2',{},'Timeline by worker'), timeline()), h('div',{class:'card w6'}, h('h2',{},'Workers'), workers()));
|
|
787
|
+
if(W.durations!==false) cards.push(h('div',{class:W.timeline!==false?'card w6':'card w12'}, h('h2',{},'Duration spread'), histogram()));
|
|
788
|
+
return h('div',{class:'grid'}, cards);
|
|
784
789
|
}
|
|
790
|
+
function hasTimelineView(){ return data.options.widgets.timeline!==false || data.options.widgets.durations!==false; }
|
|
785
791
|
function stripe(){
|
|
786
792
|
const s=data.stats, f=s.failed+s.timedOut+s.interrupted, tot=s.total||1;
|
|
787
793
|
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)})));
|
|
@@ -808,10 +814,11 @@ function header(){
|
|
|
808
814
|
[['lab','Blue'],['ocean','Ocean'],['ember','Ember'],['mono','Mono']].map(([v,l])=>h('option',{value:v,selected:html.getAttribute('data-palette')===v},l)));
|
|
809
815
|
const copy = h('button',{class:'btn',onclick:e=>copyText(summaryMarkdown(),e.currentTarget,'Copied for Slack')}, 'Copy summary');
|
|
810
816
|
return h('header',{class:'hdr'},
|
|
811
|
-
data.options.logo ? h('img',{src:data.options.logo,alt:''}) : h('div',{class:'logo',title:'Generated by reportingLabs','aria-label':'reportingLabs'}, document.getElementById('rl-logo').content.cloneNode(true)),
|
|
817
|
+
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)),
|
|
812
818
|
data.options.project ? h('div',{class:'proj-blk'}, h('h1',{}, data.title),
|
|
813
819
|
h('div',{class:'sub'}, [data.options.project.name, data.options.project.version?'v'+data.options.project.version:null, data.options.project.team].filter(Boolean).join(' · '),
|
|
814
|
-
data.options.project.url? [' · ', h('a',{href:data.options.project.url,target:'_blank',rel:'noopener'}, data.options.project.url.replace(/^https?:\/\//,''))] : null)
|
|
820
|
+
data.options.project.url? [' · ', h('a',{href:data.options.project.url,target:'_blank',rel:'noopener'}, data.options.project.url.replace(/^https?:\/\//,''))] : null),
|
|
821
|
+
data.options.project.description? h('div',{class:'sub desc'}, data.options.project.description) : null)
|
|
815
822
|
: h('h1',{}, data.title),
|
|
816
823
|
h('div',{class:'hmeta'}, Object.entries(data.metadata).map(([k,v])=>h('span',{class:'chip'}, k+' ', h('b',{},v)))),
|
|
817
824
|
h('div',{class:'spacer'}),
|
|
@@ -1041,7 +1048,7 @@ function hero(){
|
|
|
1041
1048
|
else delta=ok? 'Nothing to worry about.' : failed+' failed'+(s.flaky?', '+s.flaky+' flaky':'')+(s.skipped?', '+s.skipped+' skipped':'');
|
|
1042
1049
|
return h('button',{class:'pill kpi hero','aria-pressed':state.status==='all','data-k':'all',title:'Show all tests',onclick:()=>{state.status='all';refresh();showView('tests');}},
|
|
1043
1050
|
spark(hseries(e=>{ const rn=e.total-e.skipped; return rn?Math.round(e.passed/rn*100):0; }),'#fff'),
|
|
1044
|
-
ring,
|
|
1051
|
+
data.options.widgets.outcome!==false? ring : null,
|
|
1045
1052
|
h('div',{class:'body'},
|
|
1046
1053
|
h('div',{class:'l'}, ok? 'All passed' : 'Pass rate'),
|
|
1047
1054
|
h('div',{class:'v'}, s.passed, h('small',{}, 'of '+ran+' ran')),
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ReportingLabsOptions {
|
|
2
2
|
/** Report title shown in the header. Default: "Test report" */
|
|
3
3
|
title?: string;
|
|
4
|
-
/**
|
|
4
|
+
/** Your logo: a file path relative to the config (e.g. 'logo.png', embedded into the report), an https URL, or a data URI. */
|
|
5
5
|
logo?: string;
|
|
6
6
|
/** Override the palette accent with your brand color, e.g. '#7C3AED'. Default: the palette's own accent (royal blue for `lab`). */
|
|
7
7
|
accent?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|