reporting-labs 0.6.8 → 0.6.9
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/dist/template.html +35 -7
- package/dist/template.js +35 -7
- package/package.json +1 -1
package/dist/template.html
CHANGED
|
@@ -549,8 +549,10 @@ html,body{-webkit-font-smoothing:antialiased}
|
|
|
549
549
|
.clu2 li{display:grid;grid-template-columns:44px 1fr;gap:12px;padding:11px 0;border-top:1px solid var(--line)} .clu2 li:first-child{border-top:0;padding-top:2px}
|
|
550
550
|
.clu2 .n{font-size:20px;font-weight:600;color:var(--fail);font-variant-numeric:tabular-nums;line-height:1.1} .clu2 .n small{display:block;font-size:10.5px;color:var(--ink-3);font-weight:500;letter-spacing:.04em;margin-top:1px}
|
|
551
551
|
.clu2 .b{min-width:0} .clu2 .msg{font:12.5px/1.45 var(--mono);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
552
|
-
.clu2 .who{font-size:12.5px;color:var(--ink-2);margin-top:
|
|
553
|
-
.clu2 .who
|
|
552
|
+
.clu2 .who{font-size:12.5px;color:var(--ink-2);margin-top:5px;line-height:1.45;display:flex;flex-direction:column;align-items:flex-start;gap:3px}
|
|
553
|
+
.clu2 .who button{color:var(--accent-2);font-weight:500;text-align:left;white-space:normal;max-width:100%;padding:0 0 0 14px;text-indent:-14px} .clu2 .who button .tri{display:inline-block;width:14px;text-indent:0;color:var(--ink-3);font-weight:400}
|
|
554
|
+
.clu2 .who button:hover{text-decoration:underline} .clu2 .who button small{color:var(--ink-3);font-weight:400;font-size:11px}
|
|
555
|
+
.clu2 .who button.ex{color:var(--ink-3);font-weight:400;cursor:pointer;text-indent:0} .clu2 .who button.ex:hover{color:var(--accent-2);text-decoration:underline}
|
|
554
556
|
@media (max-width:640px){.attn2 .ow{display:none} .attn2 .pr{width:72px} .attn2 .pr small{display:none}}
|
|
555
557
|
/* ---------- run status, errors outside tests, snippets ---------- */
|
|
556
558
|
.att-err{padding:12px 14px;background:var(--fail-bg);color:var(--ink);border-radius:8px 8px 0 0;line-height:1.5} .att-err code{font:12px var(--mono);background:var(--surface);padding:1px 5px;border-radius:4px}
|
|
@@ -1300,17 +1302,43 @@ function errorSignature(msg){
|
|
|
1300
1302
|
}
|
|
1301
1303
|
function failureClusters(){
|
|
1302
1304
|
const m=new Map();
|
|
1303
|
-
for(const t of data.tests){ if(!isFail(t.outcome)) continue; const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; const x=e&&e.explain; const
|
|
1305
|
+
for(const t of data.tests){ if(!isFail(t.outcome)) continue; const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; const x=e&&e.explain; const msg=e?e.message:'(no error message)';
|
|
1306
|
+
// One cluster per root cause: the explain kind plus what it points at. An explanation that points at
|
|
1307
|
+
// nothing (a plain assertion) falls back to the message shape, so two different assertions stay apart.
|
|
1308
|
+
const sig=x&&!/^(thrown|script)$/.test(x.kind)? x.kind+'|'+(x.locator||x.url||'')+'|'+(x.matcher||x.action||'')+(x.locator||x.url?'':'|'+messageShape(msg)) : errorSignature(msg);
|
|
1309
|
+
const c=m.get(sig)||{sig,sample:msg.split('\n').map(l=>l.trim()).filter(Boolean).slice(0,3).join('\n'),why:x||null,tests:[]}; c.tests.push(t); m.set(sig,c); }
|
|
1304
1310
|
return [...m.values()].sort((a,b)=>b.tests.length-a.tests.length);
|
|
1305
1311
|
}
|
|
1312
|
+
// The message line shown under a cluster's explanation: the first line that says something the
|
|
1313
|
+
// explanation does not already say. Hamcrest's "Expected: is <200>" / "but: was <404>" is fully
|
|
1314
|
+
// covered by "The value was wrong: expected <200>, got <404>." so nothing is repeated.
|
|
1315
|
+
// The shape of a message: its first lines with numbers and quoted values blanked, so two assertions on
|
|
1316
|
+
// different values of the same kind cluster together, and assertions on different things stay apart.
|
|
1317
|
+
function messageShape(msg){ return String(msg||'').split('\n').map(l=>l.trim()).filter(Boolean).slice(0,3).map(errorSignature).join(' / '); }
|
|
1318
|
+
function cluToks(s){ const stop=/^(expected|expecting|but|is|was|were|received|actual|to|be|the|a|an|of|and|not|did|got|value|wrong|equal|have)$/; return new Set(String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,' ').split(' ').filter(w=>w&&!stop.test(w))); }
|
|
1319
|
+
function clusterLine(c){
|
|
1320
|
+
const lines=c.sample.split('\n').filter(Boolean); if(!c.why) return lines[0]||c.sample;
|
|
1321
|
+
const have=cluToks(c.why.summary);
|
|
1322
|
+
// "Error: " / "AssertionError: " prefixes carry nothing; the expect(received).toBe(expected) hint line of
|
|
1323
|
+
// Jest-style matchers is a header, not a detail.
|
|
1324
|
+
const bare=l=>l.replace(/^[\w.$]*(Error|Exception|Failure|Throwable)(:\s*|$)/,'');
|
|
1325
|
+
return lines.find(l=>{ const b=bare(l); if(/^expect\(.*\)\.[\w.]+\(.*\)/.test(b)) return false; const t=[...cluToks(b)]; return t.length && t.some(w=>!have.has(w)); })||null;
|
|
1326
|
+
}
|
|
1327
|
+
// The tests of a cluster, six by name and "+N more" for the rest; the +N more button expands the list
|
|
1328
|
+
// in place and "show fewer" folds it again. Each name opens that test.
|
|
1329
|
+
function clusterWho(byTitle, all){
|
|
1330
|
+
const entries=[...byTitle.entries()], names=all?entries:entries.slice(0,6), extra=entries.length-names.length;
|
|
1331
|
+
const who=names.map(([title,ts])=>h('button',{onclick:()=>select(ts[0].id),title:ts.map(t=>t.project).join(', ')}, h('span',{class:'tri'},'›'), title, data.projects.length>1&&ts.length>1? h('small',{},' ×'+ts.length) : null));
|
|
1332
|
+
const swap=(ev,toAll)=>{ const d=ev.currentTarget.closest('.who'); d.replaceChildren(); for(const n of clusterWho(byTitle,toAll)) d.append(n); };
|
|
1333
|
+
if(extra>0) who.push(h('button',{class:'ex',onclick:ev=>swap(ev,true),title:'Show all '+entries.length+' tests'},'+'+extra+' more'));
|
|
1334
|
+
else if(all&&entries.length>6) who.push(h('button',{class:'ex',onclick:ev=>swap(ev,false)},'show fewer'));
|
|
1335
|
+
return who;
|
|
1336
|
+
}
|
|
1306
1337
|
function clustersView(cl){
|
|
1307
1338
|
return h('ol',{class:'clu2'}, cl.map(c=>{
|
|
1308
1339
|
const byTitle=new Map(); for(const t of c.tests){ const g=byTitle.get(t.title)||[]; g.push(t); byTitle.set(t.title,g); }
|
|
1309
|
-
const names=[...byTitle.entries()].slice(0,6), extra=byTitle.size-names.length;
|
|
1310
|
-
const who=[]; names.forEach(([title,ts],k)=>{ if(k) who.push(h('span',{class:'sep'},'·')); who.push(h('button',{onclick:()=>select(ts[0].id),title:ts.map(t=>t.project).join(', ')}, title, data.projects.length>1&&ts.length>1? h('small',{},' ×'+ts.length) : null)); });
|
|
1311
|
-
if(extra>0) who.push(h('span',{class:'sep'},'·'), h('span',{class:'ex'},'+'+extra+' more'));
|
|
1312
1340
|
return h('li',{}, h('span',{class:'n'}, c.tests.length, h('small',{},c.tests.length===1?'test':'tests')),
|
|
1313
|
-
h('div',{class:'b'}, c.why? h('div',{class:'why-c'}, h('span',{class:'why-k'},c.why.label), ' ', c.why.summary) : null, h('div',{class:'msg',title:c.sample}, c
|
|
1341
|
+
h('div',{class:'b'}, c.why? h('div',{class:'why-c'}, h('span',{class:'why-k'},c.why.label), ' ', c.why.summary) : null, (line=>line? h('div',{class:'msg',title:c.sample}, line):null)(clusterLine(c)), h('div',{class:'who'}, clusterWho(byTitle,false))));
|
|
1314
1342
|
}));
|
|
1315
1343
|
}
|
|
1316
1344
|
function trend(){
|
package/dist/template.js
CHANGED
|
@@ -620,8 +620,10 @@ html,body{-webkit-font-smoothing:antialiased}
|
|
|
620
620
|
.clu2 li{display:grid;grid-template-columns:44px 1fr;gap:12px;padding:11px 0;border-top:1px solid var(--line)} .clu2 li:first-child{border-top:0;padding-top:2px}
|
|
621
621
|
.clu2 .n{font-size:20px;font-weight:600;color:var(--fail);font-variant-numeric:tabular-nums;line-height:1.1} .clu2 .n small{display:block;font-size:10.5px;color:var(--ink-3);font-weight:500;letter-spacing:.04em;margin-top:1px}
|
|
622
622
|
.clu2 .b{min-width:0} .clu2 .msg{font:12.5px/1.45 var(--mono);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
623
|
-
.clu2 .who{font-size:12.5px;color:var(--ink-2);margin-top:
|
|
624
|
-
.clu2 .who
|
|
623
|
+
.clu2 .who{font-size:12.5px;color:var(--ink-2);margin-top:5px;line-height:1.45;display:flex;flex-direction:column;align-items:flex-start;gap:3px}
|
|
624
|
+
.clu2 .who button{color:var(--accent-2);font-weight:500;text-align:left;white-space:normal;max-width:100%;padding:0 0 0 14px;text-indent:-14px} .clu2 .who button .tri{display:inline-block;width:14px;text-indent:0;color:var(--ink-3);font-weight:400}
|
|
625
|
+
.clu2 .who button:hover{text-decoration:underline} .clu2 .who button small{color:var(--ink-3);font-weight:400;font-size:11px}
|
|
626
|
+
.clu2 .who button.ex{color:var(--ink-3);font-weight:400;cursor:pointer;text-indent:0} .clu2 .who button.ex:hover{color:var(--accent-2);text-decoration:underline}
|
|
625
627
|
@media (max-width:640px){.attn2 .ow{display:none} .attn2 .pr{width:72px} .attn2 .pr small{display:none}}
|
|
626
628
|
/* ---------- run status, errors outside tests, snippets ---------- */
|
|
627
629
|
.att-err{padding:12px 14px;background:var(--fail-bg);color:var(--ink);border-radius:8px 8px 0 0;line-height:1.5} .att-err code{font:12px var(--mono);background:var(--surface);padding:1px 5px;border-radius:4px}
|
|
@@ -1366,17 +1368,43 @@ function errorSignature(msg){
|
|
|
1366
1368
|
}
|
|
1367
1369
|
function failureClusters(){
|
|
1368
1370
|
const m=new Map();
|
|
1369
|
-
for(const t of data.tests){ if(!isFail(t.outcome)) continue; const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; const x=e&&e.explain; const
|
|
1371
|
+
for(const t of data.tests){ if(!isFail(t.outcome)) continue; const r=t.results[t.results.length-1]; const e=r&&r.errors[0]; const x=e&&e.explain; const msg=e?e.message:'(no error message)';
|
|
1372
|
+
// One cluster per root cause: the explain kind plus what it points at. An explanation that points at
|
|
1373
|
+
// nothing (a plain assertion) falls back to the message shape, so two different assertions stay apart.
|
|
1374
|
+
const sig=x&&!/^(thrown|script)$/.test(x.kind)? x.kind+'|'+(x.locator||x.url||'')+'|'+(x.matcher||x.action||'')+(x.locator||x.url?'':'|'+messageShape(msg)) : errorSignature(msg);
|
|
1375
|
+
const c=m.get(sig)||{sig,sample:msg.split('\n').map(l=>l.trim()).filter(Boolean).slice(0,3).join('\n'),why:x||null,tests:[]}; c.tests.push(t); m.set(sig,c); }
|
|
1370
1376
|
return [...m.values()].sort((a,b)=>b.tests.length-a.tests.length);
|
|
1371
1377
|
}
|
|
1378
|
+
// The message line shown under a cluster's explanation: the first line that says something the
|
|
1379
|
+
// explanation does not already say. Hamcrest's "Expected: is <200>" / "but: was <404>" is fully
|
|
1380
|
+
// covered by "The value was wrong: expected <200>, got <404>." so nothing is repeated.
|
|
1381
|
+
// The shape of a message: its first lines with numbers and quoted values blanked, so two assertions on
|
|
1382
|
+
// different values of the same kind cluster together, and assertions on different things stay apart.
|
|
1383
|
+
function messageShape(msg){ return String(msg||'').split('\n').map(l=>l.trim()).filter(Boolean).slice(0,3).map(errorSignature).join(' / '); }
|
|
1384
|
+
function cluToks(s){ const stop=/^(expected|expecting|but|is|was|were|received|actual|to|be|the|a|an|of|and|not|did|got|value|wrong|equal|have)$/; return new Set(String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,' ').split(' ').filter(w=>w&&!stop.test(w))); }
|
|
1385
|
+
function clusterLine(c){
|
|
1386
|
+
const lines=c.sample.split('\n').filter(Boolean); if(!c.why) return lines[0]||c.sample;
|
|
1387
|
+
const have=cluToks(c.why.summary);
|
|
1388
|
+
// "Error: " / "AssertionError: " prefixes carry nothing; the expect(received).toBe(expected) hint line of
|
|
1389
|
+
// Jest-style matchers is a header, not a detail.
|
|
1390
|
+
const bare=l=>l.replace(/^[\w.$]*(Error|Exception|Failure|Throwable)(:\s*|$)/,'');
|
|
1391
|
+
return lines.find(l=>{ const b=bare(l); if(/^expect\(.*\)\.[\w.]+\(.*\)/.test(b)) return false; const t=[...cluToks(b)]; return t.length && t.some(w=>!have.has(w)); })||null;
|
|
1392
|
+
}
|
|
1393
|
+
// The tests of a cluster, six by name and "+N more" for the rest; the +N more button expands the list
|
|
1394
|
+
// in place and "show fewer" folds it again. Each name opens that test.
|
|
1395
|
+
function clusterWho(byTitle, all){
|
|
1396
|
+
const entries=[...byTitle.entries()], names=all?entries:entries.slice(0,6), extra=entries.length-names.length;
|
|
1397
|
+
const who=names.map(([title,ts])=>h('button',{onclick:()=>select(ts[0].id),title:ts.map(t=>t.project).join(', ')}, h('span',{class:'tri'},'›'), title, data.projects.length>1&&ts.length>1? h('small',{},' ×'+ts.length) : null));
|
|
1398
|
+
const swap=(ev,toAll)=>{ const d=ev.currentTarget.closest('.who'); d.replaceChildren(); for(const n of clusterWho(byTitle,toAll)) d.append(n); };
|
|
1399
|
+
if(extra>0) who.push(h('button',{class:'ex',onclick:ev=>swap(ev,true),title:'Show all '+entries.length+' tests'},'+'+extra+' more'));
|
|
1400
|
+
else if(all&&entries.length>6) who.push(h('button',{class:'ex',onclick:ev=>swap(ev,false)},'show fewer'));
|
|
1401
|
+
return who;
|
|
1402
|
+
}
|
|
1372
1403
|
function clustersView(cl){
|
|
1373
1404
|
return h('ol',{class:'clu2'}, cl.map(c=>{
|
|
1374
1405
|
const byTitle=new Map(); for(const t of c.tests){ const g=byTitle.get(t.title)||[]; g.push(t); byTitle.set(t.title,g); }
|
|
1375
|
-
const names=[...byTitle.entries()].slice(0,6), extra=byTitle.size-names.length;
|
|
1376
|
-
const who=[]; names.forEach(([title,ts],k)=>{ if(k) who.push(h('span',{class:'sep'},'·')); who.push(h('button',{onclick:()=>select(ts[0].id),title:ts.map(t=>t.project).join(', ')}, title, data.projects.length>1&&ts.length>1? h('small',{},' ×'+ts.length) : null)); });
|
|
1377
|
-
if(extra>0) who.push(h('span',{class:'sep'},'·'), h('span',{class:'ex'},'+'+extra+' more'));
|
|
1378
1406
|
return h('li',{}, h('span',{class:'n'}, c.tests.length, h('small',{},c.tests.length===1?'test':'tests')),
|
|
1379
|
-
h('div',{class:'b'}, c.why? h('div',{class:'why-c'}, h('span',{class:'why-k'},c.why.label), ' ', c.why.summary) : null, h('div',{class:'msg',title:c.sample}, c
|
|
1407
|
+
h('div',{class:'b'}, c.why? h('div',{class:'why-c'}, h('span',{class:'why-k'},c.why.label), ' ', c.why.summary) : null, (line=>line? h('div',{class:'msg',title:c.sample}, line):null)(clusterLine(c)), h('div',{class:'who'}, clusterWho(byTitle,false))));
|
|
1380
1408
|
}));
|
|
1381
1409
|
}
|
|
1382
1410
|
function trend(){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
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",
|