reporting-labs 0.6.7 → 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/README.md +4 -1
- package/dist/reporter.d.ts +7 -0
- package/dist/reporter.js +56 -1
- package/dist/template.html +35 -7
- package/dist/template.js +35 -7
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -303,7 +303,10 @@ Every option is optional. `npx reporting-labs init` writes them all, with commen
|
|
|
303
303
|
| `title` | `'Test report'` | Title in the header |
|
|
304
304
|
| `logo` | – | Your logo next to the title: `'logo.png'` (a file next to the config, embedded in the report) or an https URL |
|
|
305
305
|
| `project` | – | `{ name, version, team, url }` shown under the title |
|
|
306
|
-
| `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 |
|
|
306
|
+
| `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. The environment name is also read from the process environment and wins over `env` here: `ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV` and friends, or any variable ending in `_ENV` / `_ENVIRONMENT` (`OPENCART_ENV`), so a config that says `local` still labels CI reports `dev`, `qa`, `stage` |
|
|
307
|
+
| `envVar` | – | Name of the variable that holds the environment name, when the detection cannot guess it |
|
|
308
|
+
|
|
309
|
+
**Runtime overrides.** `REPORTING_LABS_METADATA_<KEY>` sets a header chip from the environment (`REPORTING_LABS_METADATA_ENV=qa`, `REPORTING_LABS_METADATA_RELEASE=2.3`) and `REPORTING_LABS_TITLE`, `_THEME`, `_PALETTE`, `_ACCENT`, `_LOGO` the matching option; they win over the config. The env chip resolves in this order: `REPORTING_LABS_METADATA_ENV`, the variable `envVar` names, the conventional names (`ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV`, `CI_ENVIRONMENT_NAME`, anything ending in `_ENV`), then `metadata.env` in the config.
|
|
307
310
|
| `env` | – | Extra rows on the Environment card |
|
|
308
311
|
| `links` | `{}` | Turn meta keys into links. `{id}` is replaced by the value. An object `{ url, display }` builds the URL from several fields of an object passed to `meta()`, see below |
|
|
309
312
|
| `maskKeys` | `[]` | Extra keys to mask as `****` |
|
package/dist/reporter.d.ts
CHANGED
|
@@ -54,3 +54,10 @@ export default class ReportingLabsReporter implements Reporter {
|
|
|
54
54
|
private serializeStep;
|
|
55
55
|
private serializeAttachment;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The environment name the run was pointed at, from the process environment: the variable named
|
|
59
|
+
* by `envVar` if given, else the usual names (ENV, TEST_ENV, APP_ENV, TARGET_ENV, …), else any
|
|
60
|
+
* variable whose name ends in _ENV or _ENVIRONMENT (OPENCART_ENV, app_env). Every project names
|
|
61
|
+
* it differently; this finds it without being told. Undefined when nothing fits.
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectEnvName(env: NodeJS.ProcessEnv, envVar?: string): string | undefined;
|
package/dist/reporter.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.detectEnvName = detectEnvName;
|
|
36
37
|
const fs = __importStar(require("fs"));
|
|
37
38
|
const path = __importStar(require("path"));
|
|
38
39
|
const os = __importStar(require("os"));
|
|
@@ -56,7 +57,32 @@ class ReportingLabsReporter {
|
|
|
56
57
|
this.masker = (0, mask_1.makeMasker)();
|
|
57
58
|
this.globalErrors = [];
|
|
58
59
|
this.globalOutput = [];
|
|
59
|
-
|
|
60
|
+
// The env chip. A runtime variable beats the config file, the same precedence as everything
|
|
61
|
+
// else: the ENV / TEST_ENV / ENVIRONMENT / APP_ENV a CI job exports wins over metadata.env in
|
|
62
|
+
// the config, so a config that says env: 'local' still labels the pipeline's reports dev, qa,
|
|
63
|
+
// stage without anyone touching it. The config value applies when no variable is set.
|
|
64
|
+
// Runtime overrides, highest precedence: REPORTING_LABS_METADATA_<KEY> sets a header chip
|
|
65
|
+
// (REPORTING_LABS_METADATA_ENV=qa, REPORTING_LABS_METADATA_RELEASE=2.3) and REPORTING_LABS_TITLE /
|
|
66
|
+
// _THEME / _PALETTE / _ACCENT / _LOGO the matching option, so a pipeline can label a run without
|
|
67
|
+
// touching the config. Same contract as the Java reporter's REPORTING_LABS_* variables.
|
|
68
|
+
const metadata = { ...(options.metadata ?? {}) };
|
|
69
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
70
|
+
const m = /^REPORTING_LABS_METADATA_([A-Z0-9_]+)$/.exec(k);
|
|
71
|
+
if (m && v && v.trim())
|
|
72
|
+
metadata[m[1].toLowerCase()] = v.trim();
|
|
73
|
+
}
|
|
74
|
+
// Below the explicit override, the environment name the run was pointed at, found by convention
|
|
75
|
+
// (ENV, TEST_ENV, APP_ENV, TARGET_ENV, anything ending in _ENV) or the variable `envVar` names.
|
|
76
|
+
// It beats metadata.env in the config: a runtime value wins over the file, as everywhere else.
|
|
77
|
+
if (!process.env.REPORTING_LABS_METADATA_ENV) {
|
|
78
|
+
const detected = detectEnvName(process.env, options.envVar);
|
|
79
|
+
if (detected)
|
|
80
|
+
metadata.env = detected;
|
|
81
|
+
}
|
|
82
|
+
const scalar = (name, current) => { const v = process.env['REPORTING_LABS_' + name]; return v && v.trim() ? v.trim() : current; };
|
|
83
|
+
this.options = { ...options, metadata,
|
|
84
|
+
title: scalar('TITLE', options.title), theme: scalar('THEME', options.theme), palette: scalar('PALETTE', options.palette),
|
|
85
|
+
accent: scalar('ACCENT', options.accent), logo: scalar('LOGO', options.logo) };
|
|
60
86
|
this.masker = (0, mask_1.makeMasker)(options.maskKeys ?? [], { knownValues: options.maskValues ?? [], fromEnv: options.maskFromEnv !== false });
|
|
61
87
|
}
|
|
62
88
|
printsToStdio() { return false; }
|
|
@@ -595,6 +621,35 @@ class ReportingLabsReporter {
|
|
|
595
621
|
}
|
|
596
622
|
exports.default = ReportingLabsReporter;
|
|
597
623
|
/** Run number from the CI system, used to label history entries when metadata.build is not set. */
|
|
624
|
+
/** Variables whose name ends in ENV but never hold an environment name. */
|
|
625
|
+
const NOT_AN_ENV_VAR = new Set(['GITHUB_ENV', 'NODE_ENV', 'BASH_ENV', 'VIRTUAL_ENV', 'CONDA_DEFAULT_ENV', 'RUNNER_ENVIRONMENT', 'PIPENV_ACTIVE', 'ZSH_ENV', 'JAVA_ENV', 'DOTNET_ENVIRONMENT', 'ASPNETCORE_ENVIRONMENT', 'HOSTING_ENVIRONMENT']);
|
|
626
|
+
/** Names every project seems to pick first, in order. */
|
|
627
|
+
const ENV_VAR_NAMES = ['ENV', 'TEST_ENV', 'ENVIRONMENT', 'APP_ENV', 'TARGET_ENV', 'RUN_ENV', 'DEPLOY_ENV', 'ENV_NAME', 'TEST_ENVIRONMENT', 'TARGET_ENVIRONMENT', 'CI_ENVIRONMENT_NAME', 'DEPLOYMENT_ENVIRONMENT', 'STAGE'];
|
|
628
|
+
/** dev, qa, stage-2, app_qa, prod-eu: a short token, never a path, URL or sentence. */
|
|
629
|
+
const looksLikeEnvName = (v) => !!v && /^[A-Za-z][\w.-]{0,31}$/.test(v.trim());
|
|
630
|
+
/**
|
|
631
|
+
* The environment name the run was pointed at, from the process environment: the variable named
|
|
632
|
+
* by `envVar` if given, else the usual names (ENV, TEST_ENV, APP_ENV, TARGET_ENV, …), else any
|
|
633
|
+
* variable whose name ends in _ENV or _ENVIRONMENT (OPENCART_ENV, app_env). Every project names
|
|
634
|
+
* it differently; this finds it without being told. Undefined when nothing fits.
|
|
635
|
+
*/
|
|
636
|
+
function detectEnvName(env, envVar) {
|
|
637
|
+
const get = (name) => { const v = env[name] ?? env[name.toLowerCase()]; return looksLikeEnvName(v) ? v.trim() : undefined; };
|
|
638
|
+
if (envVar)
|
|
639
|
+
return get(envVar) ?? (looksLikeEnvName(env[envVar]) ? env[envVar].trim() : undefined);
|
|
640
|
+
for (const n of ENV_VAR_NAMES) {
|
|
641
|
+
const v = get(n);
|
|
642
|
+
if (v)
|
|
643
|
+
return v;
|
|
644
|
+
}
|
|
645
|
+
const wild = Object.keys(env).filter(k => /_(ENV|ENVIRONMENT|ENV_NAME)$/i.test(k) && !NOT_AN_ENV_VAR.has(k.toUpperCase())).sort();
|
|
646
|
+
for (const k of wild) {
|
|
647
|
+
const v = env[k];
|
|
648
|
+
if (looksLikeEnvName(v))
|
|
649
|
+
return v.trim();
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
598
653
|
function ciRunLabel(env) {
|
|
599
654
|
const n = env.GITHUB_RUN_NUMBER || env.BUILD_NUMBER || env.CI_PIPELINE_IID || env.CIRCLE_BUILD_NUM || env.BUILD_BUILDNUMBER || env.BITBUCKET_BUILD_NUMBER;
|
|
600
655
|
return n ? `#${n}` : undefined;
|
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/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface ReportingLabsOptions {
|
|
|
25
25
|
embedVideos?: boolean;
|
|
26
26
|
/** Key/value shown in the header (env, branch, build...). `build` labels the run in the history; when it is not set the CI run number is used. */
|
|
27
27
|
metadata?: Record<string, string>;
|
|
28
|
+
/** Name of the environment variable that holds the environment name, when it is not one the reporter finds by itself (ENV, TEST_ENV, APP_ENV, anything ending in _ENV). */
|
|
29
|
+
envVar?: string;
|
|
28
30
|
/** Extra sections rendered below the summary. HTML is allowed. */
|
|
29
31
|
sections?: Array<{
|
|
30
32
|
title: string;
|
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",
|