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 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` | – | Path, URL or data URI of your 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'` |
@@ -9,7 +9,7 @@ const config: ReportingLabsOptions = {
9
9
 
10
10
  // ── Look ─────────────────────────────────────────────────────────────────────
11
11
  title: 'My app – regression', // shown in the header
12
- // logo: 'https://example.com/logo.svg', // path, URL or data URI; shown next to the title
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
@@ -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
- return h('div',{class:'grid'}, h('div',{class:'card w12'}, h('h2',{},'Timeline by worker'), timeline()), h('div',{class:'card w6'}, h('h2',{},'Workers'), workers()), h('div',{class:'card w6'}, h('h2',{},'Duration spread'), histogram()));
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
- /** Path or data-URI of a logo image. Optional. */
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.0",
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",