reporting-labs 0.6.1 → 0.6.2
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 +23 -2
- package/dist/auto.js +52 -15
- package/dist/meta.d.ts +3 -0
- package/dist/meta.js +7 -1
- package/dist/reporter.d.ts +9 -1
- package/dist/reporter.js +65 -5
- package/dist/template.html +2 -2
- package/dist/template.js +2 -2
- package/dist/types.d.ts +22 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,28 @@ One line per test. With this the report can rank failures by priority, group the
|
|
|
88
88
|
|
|
89
89
|
- Known keys: `priority`, `severity`, `owner`, `feature`, `epic`, `story`, `issue`, `component`, `team`. Any other key you pass is shown too.
|
|
90
90
|
- Your Playwright tags like `@sanity` or `@regression` stay as they are and still show on the test.
|
|
91
|
-
- To make story and epic keys clickable, set `links` in the config: `links: { story: 'https://yourteam.atlassian.net/browse/{id}' }`.
|
|
91
|
+
- To make story and epic keys clickable, set `links` in the config: `links: { story: 'https://yourteam.atlassian.net/browse/{id}' }`. Several ids (`story: ['SHOP-1', 'SHOP-2']`) become one link each.
|
|
92
|
+
- A link whose URL needs more than the shown value (a workspace, a project, an organisation) takes an object. Any tool, any URL shape: every `{placeholder}` in `url` is filled from the object you pass in `meta()`, `display` is what the report shows (default `{id}`), the other fields only build the URL. ALM Octane / ValueEdge as an example:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// reporting-labs.config.ts
|
|
96
|
+
links: {
|
|
97
|
+
octaneTestCase: {
|
|
98
|
+
url: 'https://oss.valueedge.com/ui/?p={p}#/entity-navigation?entityType=test&id={id}',
|
|
99
|
+
display: '{id}',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// tests/notifications.spec.ts
|
|
106
|
+
test('TC003 - manual trigger with all notifications disabled', async ({ page }) => {
|
|
107
|
+
meta({ priority: 'P1', owner: 'chetan', octaneTestCase: { id: '58966', p: '4001/14014' } });
|
|
108
|
+
// ... your test as usual
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The test shows a chip **octaneTestCase 58966**; clicking it opens the full URL. `p` never appears in the report and can differ per test. The same shape covers Azure DevOps (`url: 'https://dev.azure.com/{org}/{project}/_workitems/edit/{id}', display: 'AB#{id}'`) or any in-house tool. Jira, TestRail, Xray and Zephyr need only the plain `{id}` string.
|
|
92
113
|
- Forgot one? After every run the console lists the tests that have no `meta()`, with file and line. Turn it off with `warnMissingMeta: false`.
|
|
93
114
|
- `priority`, `severity`, `feature` and `owner` each get a tab in the Breakdown chart and a filter on the Tests page. Want the same for your own key, say `meta({ team: 'web' })`? Add it to `dimensions` in the config: `dimensions: ['priority', 'severity', 'feature', 'owner', 'team']`.
|
|
94
115
|
|
|
@@ -284,7 +305,7 @@ Every option is optional. `npx reporting-labs init` writes them all, with commen
|
|
|
284
305
|
| `project` | – | `{ name, version, team, url }` shown under the title |
|
|
285
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 |
|
|
286
307
|
| `env` | – | Extra rows on the Environment card |
|
|
287
|
-
| `links` | `{}` | Turn meta keys into links. `{id}` is replaced by the value |
|
|
308
|
+
| `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 |
|
|
288
309
|
| `maskKeys` | `[]` | Extra keys to mask as `****` |
|
|
289
310
|
| `dimensions` | `['priority','severity','feature','owner']` | Which `meta()` keys get a tab in the Breakdown chart and a dropdown filter on the Tests page. Add your own key, e.g. `'team'`, to get a chart for it |
|
|
290
311
|
| `dimensionOrder` | P0…P4, blocker…trivial | The order values appear in those charts and filters. Only needed for your own values, e.g. `{ severity: ['high','medium','low'] }` |
|
package/dist/auto.js
CHANGED
|
@@ -154,28 +154,65 @@ function patchContext(ctx) {
|
|
|
154
154
|
};
|
|
155
155
|
proto[PATCHED] = true;
|
|
156
156
|
}
|
|
157
|
+
/** Wrap `obj[name]` (found on the prototype chain) so `after(result)` sees each resolved return value. */
|
|
158
|
+
function wrapAsync(obj, name, after) {
|
|
159
|
+
let proto = obj;
|
|
160
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, name))
|
|
161
|
+
proto = Object.getPrototypeOf(proto);
|
|
162
|
+
if (!proto || typeof proto[name] !== 'function' || proto[name][PATCHED])
|
|
163
|
+
return;
|
|
164
|
+
const original = proto[name];
|
|
165
|
+
const wrapped = async function (...args) {
|
|
166
|
+
const result = await original.apply(this, args);
|
|
167
|
+
try {
|
|
168
|
+
after(result);
|
|
169
|
+
}
|
|
170
|
+
catch { /* never break the caller */ }
|
|
171
|
+
return result;
|
|
172
|
+
};
|
|
173
|
+
wrapped[PATCHED] = true;
|
|
174
|
+
proto[name] = wrapped;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Hook the places an APIRequestContext can come from, and patch the shared prototype the
|
|
178
|
+
* first time one shows up:
|
|
179
|
+
* - playwright.request.newContext() (the `request` fixture, manual contexts)
|
|
180
|
+
* - browser.newContext() / launchPersistentContext() / connect*() (page.request, context.request)
|
|
181
|
+
*
|
|
182
|
+
* No context is created here. An earlier version made a throwaway context at import time and
|
|
183
|
+
* disposed it; when that dispose raced with the test runner's tracing bookkeeping the runner
|
|
184
|
+
* tried to start a trace on a closed context and every test failed with
|
|
185
|
+
* "apiRequestContext._wrapApiCall: Target page, context or browser has been closed".
|
|
186
|
+
*/
|
|
157
187
|
function install() {
|
|
158
188
|
let api;
|
|
159
189
|
try {
|
|
160
|
-
api = pw()
|
|
190
|
+
api = pw();
|
|
161
191
|
}
|
|
162
192
|
catch {
|
|
163
193
|
return;
|
|
164
194
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
const { request, chromium, firefox, webkit } = api;
|
|
196
|
+
if (request && typeof request.newContext === 'function')
|
|
197
|
+
wrapAsync(request, 'newContext', ctx => patchContext(ctx));
|
|
198
|
+
const onBrowser = (browser) => {
|
|
199
|
+
if (!browser || typeof browser.newContext !== 'function')
|
|
200
|
+
return;
|
|
201
|
+
wrapAsync(browser, 'newContext', ctx => ctx?.request && patchContext(ctx.request));
|
|
202
|
+
try {
|
|
203
|
+
for (const ctx of browser.contexts())
|
|
204
|
+
if (ctx?.request)
|
|
205
|
+
patchContext(ctx.request);
|
|
206
|
+
}
|
|
207
|
+
catch { /* not a browser */ }
|
|
177
208
|
};
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
for (const bt of [chromium, firefox, webkit]) {
|
|
210
|
+
if (!bt)
|
|
211
|
+
continue;
|
|
212
|
+
wrapAsync(bt, 'launch', onBrowser);
|
|
213
|
+
wrapAsync(bt, 'connect', onBrowser);
|
|
214
|
+
wrapAsync(bt, 'connectOverCDP', onBrowser);
|
|
215
|
+
wrapAsync(bt, 'launchPersistentContext', ctx => ctx?.request && patchContext(ctx.request));
|
|
216
|
+
}
|
|
180
217
|
}
|
|
181
218
|
install();
|
package/dist/meta.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import type { ApiCall, TestMeta } from './types';
|
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* meta({ priority: 'P1', severity: 'critical', owner: 'naveen', feature: 'checkout', story: 'SHOP-231' });
|
|
7
|
+
*
|
|
8
|
+
* A value can be an object when the link needs more than one parameter (see `links` in the config):
|
|
9
|
+
* meta({ octaneTestCase: { id: '58966', p: '4001/14014' } });
|
|
7
10
|
*/
|
|
8
11
|
export declare function meta(values: TestMeta): void;
|
|
9
12
|
/**
|
package/dist/meta.js
CHANGED
|
@@ -16,13 +16,19 @@ function info() {
|
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* meta({ priority: 'P1', severity: 'critical', owner: 'naveen', feature: 'checkout', story: 'SHOP-231' });
|
|
19
|
+
*
|
|
20
|
+
* A value can be an object when the link needs more than one parameter (see `links` in the config):
|
|
21
|
+
* meta({ octaneTestCase: { id: '58966', p: '4001/14014' } });
|
|
19
22
|
*/
|
|
20
23
|
function meta(values) {
|
|
21
24
|
const i = info();
|
|
22
25
|
for (const [k, v] of Object.entries(values)) {
|
|
23
26
|
if (v === undefined)
|
|
24
27
|
continue;
|
|
25
|
-
|
|
28
|
+
// Objects (e.g. { id: '58966', p: '4001/14014' } for a multi-parameter link) travel as JSON.
|
|
29
|
+
// Arrays (story: ['SHOP-1', 'SHOP-2']) become one chip per value; objects (a multi-parameter link) travel as JSON.
|
|
30
|
+
const text = Array.isArray(v) ? v.map(String).join(', ') : v !== null && typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
31
|
+
i.annotations.push({ type: k.toLowerCase(), description: text });
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
/**
|
package/dist/reporter.d.ts
CHANGED
|
@@ -34,7 +34,15 @@ export default class ReportingLabsReporter implements Reporter {
|
|
|
34
34
|
private dimensions;
|
|
35
35
|
/** Keys picked up from tags and annotations even when they are not breakdown dimensions. */
|
|
36
36
|
private metaKeys;
|
|
37
|
-
/**
|
|
37
|
+
/** The link template for a (lower-cased) meta key, falling back to '*'. */
|
|
38
|
+
private linkTemplate;
|
|
39
|
+
/** `links` reduced to URL templates, the shape the template expects. */
|
|
40
|
+
private linkUrls;
|
|
41
|
+
/**
|
|
42
|
+
* Pull meta values (priority, owner, story, epic...) from annotations and tags.
|
|
43
|
+
* An object value (meta({ octaneTestCase: { id, p } })) is folded to its display text and, when the key has a
|
|
44
|
+
* link template, to a ready-made href in `links`; the other fields never show in the report.
|
|
45
|
+
*/
|
|
38
46
|
private extractMeta;
|
|
39
47
|
/** 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. */
|
|
40
48
|
private resolveLogo;
|
package/dist/reporter.js
CHANGED
|
@@ -105,6 +105,7 @@ class ReportingLabsReporter {
|
|
|
105
105
|
const last = test.results[test.results.length - 1];
|
|
106
106
|
const resultAnn = (last?.annotations ?? []);
|
|
107
107
|
const annotations = [...test.annotations, ...resultAnn.filter(a => !test.annotations.some(b => b.type === a.type && b.description === a.description))];
|
|
108
|
+
const extracted = this.extractMeta(test);
|
|
108
109
|
tests.push({
|
|
109
110
|
id: test.id,
|
|
110
111
|
key: `${project}::${file}::${[...titlePath, test.title].join(' › ')}${test.repeatEachIndex ? ' #' + (test.repeatEachIndex + 1) : ''}`,
|
|
@@ -116,7 +117,8 @@ class ReportingLabsReporter {
|
|
|
116
117
|
project,
|
|
117
118
|
tags: test.tags,
|
|
118
119
|
annotations,
|
|
119
|
-
meta:
|
|
120
|
+
meta: extracted.meta,
|
|
121
|
+
links: Object.keys(extracted.links).length ? extracted.links : undefined,
|
|
120
122
|
outcome,
|
|
121
123
|
expectedFailure,
|
|
122
124
|
note,
|
|
@@ -195,7 +197,7 @@ class ReportingLabsReporter {
|
|
|
195
197
|
...(this.options.dimensionOrder ?? {}),
|
|
196
198
|
},
|
|
197
199
|
project: this.options.project,
|
|
198
|
-
links: this.
|
|
200
|
+
links: this.linkUrls(),
|
|
199
201
|
customCss: this.options.customCss ?? '',
|
|
200
202
|
editorLinks: this.options.editorLinks ?? !process.env.CI,
|
|
201
203
|
},
|
|
@@ -352,10 +354,34 @@ class ReportingLabsReporter {
|
|
|
352
354
|
metaKeys() {
|
|
353
355
|
return [...new Set([...this.dimensions(), ...META_KEYS, ...Object.keys(this.options.links ?? {}).map(k => k.toLowerCase())])].filter(k => k !== '*');
|
|
354
356
|
}
|
|
355
|
-
/**
|
|
357
|
+
/** The link template for a (lower-cased) meta key, falling back to '*'. */
|
|
358
|
+
linkTemplate(key) {
|
|
359
|
+
let star;
|
|
360
|
+
for (const [k, v] of Object.entries(this.options.links ?? {})) {
|
|
361
|
+
if (k.toLowerCase() === key)
|
|
362
|
+
return v;
|
|
363
|
+
if (k === '*')
|
|
364
|
+
star = v;
|
|
365
|
+
}
|
|
366
|
+
return star;
|
|
367
|
+
}
|
|
368
|
+
/** `links` reduced to URL templates, the shape the template expects. */
|
|
369
|
+
linkUrls() {
|
|
370
|
+
const out = {};
|
|
371
|
+
// Meta keys are lower-cased on the way in, so link keys must be too (links: { testCaseId } did not match before).
|
|
372
|
+
for (const [k, v] of Object.entries(this.options.links ?? {}))
|
|
373
|
+
out[k.toLowerCase()] = typeof v === 'string' ? v : v.url;
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Pull meta values (priority, owner, story, epic...) from annotations and tags.
|
|
378
|
+
* An object value (meta({ octaneTestCase: { id, p } })) is folded to its display text and, when the key has a
|
|
379
|
+
* link template, to a ready-made href in `links`; the other fields never show in the report.
|
|
380
|
+
*/
|
|
356
381
|
extractMeta(test) {
|
|
357
382
|
const dims = this.metaKeys();
|
|
358
383
|
const meta = {};
|
|
384
|
+
const links = {};
|
|
359
385
|
// Tags first (describe-level, then test-level), annotations last so a test can override its describe's tags.
|
|
360
386
|
for (const raw of test.tags) {
|
|
361
387
|
const tag = raw.replace(/^@/, '');
|
|
@@ -371,10 +397,22 @@ class ReportingLabsReporter {
|
|
|
371
397
|
}
|
|
372
398
|
for (const a of test.annotations) {
|
|
373
399
|
const k = a.type.toLowerCase();
|
|
374
|
-
if (dims.includes(k)
|
|
400
|
+
if (!dims.includes(k) || !a.description)
|
|
401
|
+
continue;
|
|
402
|
+
const fields = parseObject(a.description);
|
|
403
|
+
if (!fields) {
|
|
375
404
|
meta[k] = a.description;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const tpl = this.linkTemplate(k);
|
|
408
|
+
const display = typeof tpl === 'object' && tpl.display ? tpl.display : '{id}';
|
|
409
|
+
const shown = fill(display, fields, false);
|
|
410
|
+
meta[k] = shown || String(fields.id ?? Object.values(fields)[0] ?? '');
|
|
411
|
+
const url = typeof tpl === 'string' ? tpl : tpl?.url;
|
|
412
|
+
if (url)
|
|
413
|
+
links[k] = fill(url, fields, true);
|
|
376
414
|
}
|
|
377
|
-
return meta;
|
|
415
|
+
return { meta, links };
|
|
378
416
|
}
|
|
379
417
|
/** 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. */
|
|
380
418
|
resolveLogo(logo) {
|
|
@@ -622,3 +660,25 @@ function stripAnsi(s) {
|
|
|
622
660
|
function sanitize(s) {
|
|
623
661
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
|
|
624
662
|
}
|
|
663
|
+
/** A JSON object literal in an annotation description (what meta() writes for object values), else undefined. */
|
|
664
|
+
function parseObject(text) {
|
|
665
|
+
if (!text.startsWith('{'))
|
|
666
|
+
return undefined;
|
|
667
|
+
try {
|
|
668
|
+
const v = JSON.parse(text);
|
|
669
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Replace {field} placeholders; for URLs each value is encoded except '/' and ':' so a value like '4001/14014' survives. */
|
|
676
|
+
function fill(template, fields, forUrl) {
|
|
677
|
+
return template.replace(/\{(\w+)\}/g, (_m, name) => {
|
|
678
|
+
const v = fields[name];
|
|
679
|
+
if (v === undefined || v === null)
|
|
680
|
+
return '';
|
|
681
|
+
const text = String(v);
|
|
682
|
+
return forUrl ? encodeURIComponent(text).replace(/%2F/gi, '/').replace(/%3A/gi, ':') : text;
|
|
683
|
+
});
|
|
684
|
+
}
|
package/dist/template.html
CHANGED
|
@@ -1488,8 +1488,8 @@ function renderDetail(t){
|
|
|
1488
1488
|
editorLink(t))),
|
|
1489
1489
|
h('div',{class:'badges'}, h('span',{class:'badge '+t.outcome},label[t.outcome]), t.expectedFailure? h('span',{class:'badge xfail',title:'Marked test.fail(): failing is the expected result'},'Expected failure') : null, sinceBadge(t), dots(t,10), data.bdd?h('span',{class:'badge scenario'},'Scenario'):null, t.tags.map(g=>h('span',{class:'badge tag'},g)), data.projects.length>1?h('span',{class:'badge tag'},t.project):null, h('span',{class:'loc'}, t.file+':'+t.line+' · '+ms(t.duration)+(t.outcome==='timedOut'&&t.timeout?' · timeout '+ms(t.timeout):'')+(t.retries?' · retries '+t.retries:''))));
|
|
1490
1490
|
const metaKeys=Object.keys(t.meta);
|
|
1491
|
-
const linkFor=(k,v)=>{ const tpl=data.options.links[k]||data.options.links['*']; if(tpl) return tpl.replace('{id}',encodeURIComponent(v)); if(/^https?:\/\//.test(v)) return v; return null; };
|
|
1492
|
-
if(metaKeys.length) d.append(h('div',{class:'metas'}, metaKeys.map(k=>{ const v=t.meta[k], low=/^(P[3-4]|low|minor|trivial|normal|medium)$/i.test(v), href=linkFor(k,
|
|
1491
|
+
const linkFor=(k,v)=>{ if(t.links&&t.links[k]) return t.links[k]; const tpl=data.options.links[k]||data.options.links['*']; if(tpl) return tpl.replace('{id}',encodeURIComponent(v)); if(/^https?:\/\//.test(v)) return v; return null; };
|
|
1492
|
+
if(metaKeys.length) d.append(h('div',{class:'metas'}, metaKeys.map(k=>{ const v=t.meta[k], low=/^(P[3-4]|low|minor|trivial|normal|medium)$/i.test(v); const parts=(t.links&&t.links[k])||!data.options.links[k]&&!data.options.links['*']?[v]:v.split(/,\s+/); const nodes=[]; parts.forEach((part,i)=>{ const href=linkFor(k,part); if(i) nodes.push(', '); nodes.push(href? h('a',{href,target:'_blank',rel:'noopener'},part) : h('span',{class:'v'},part)); }); return h('span',{class:'meta '+k+(low?' low':'')}, h('span',{class:'k'},k), ...nodes); })));
|
|
1493
1493
|
const otherAnn=t.annotations.filter(a=>!DIMS.includes(a.type.toLowerCase()) && !(a.type.toLowerCase() in t.meta));
|
|
1494
1494
|
if(otherAnn.length) d.append(h('h4',{},'Annotations'), h('dl',{class:'kv'}, otherAnn.map(a=>[h('dt',{},a.type),h('dd',{},a.description||'')])));
|
|
1495
1495
|
if(t.results.length>1){
|
package/dist/template.js
CHANGED
|
@@ -1554,8 +1554,8 @@ function renderDetail(t){
|
|
|
1554
1554
|
editorLink(t))),
|
|
1555
1555
|
h('div',{class:'badges'}, h('span',{class:'badge '+t.outcome},label[t.outcome]), t.expectedFailure? h('span',{class:'badge xfail',title:'Marked test.fail(): failing is the expected result'},'Expected failure') : null, sinceBadge(t), dots(t,10), data.bdd?h('span',{class:'badge scenario'},'Scenario'):null, t.tags.map(g=>h('span',{class:'badge tag'},g)), data.projects.length>1?h('span',{class:'badge tag'},t.project):null, h('span',{class:'loc'}, t.file+':'+t.line+' · '+ms(t.duration)+(t.outcome==='timedOut'&&t.timeout?' · timeout '+ms(t.timeout):'')+(t.retries?' · retries '+t.retries:''))));
|
|
1556
1556
|
const metaKeys=Object.keys(t.meta);
|
|
1557
|
-
const linkFor=(k,v)=>{ const tpl=data.options.links[k]||data.options.links['*']; if(tpl) return tpl.replace('{id}',encodeURIComponent(v)); if(/^https?:\/\//.test(v)) return v; return null; };
|
|
1558
|
-
if(metaKeys.length) d.append(h('div',{class:'metas'}, metaKeys.map(k=>{ const v=t.meta[k], low=/^(P[3-4]|low|minor|trivial|normal|medium)$/i.test(v), href=linkFor(k,
|
|
1557
|
+
const linkFor=(k,v)=>{ if(t.links&&t.links[k]) return t.links[k]; const tpl=data.options.links[k]||data.options.links['*']; if(tpl) return tpl.replace('{id}',encodeURIComponent(v)); if(/^https?:\/\//.test(v)) return v; return null; };
|
|
1558
|
+
if(metaKeys.length) d.append(h('div',{class:'metas'}, metaKeys.map(k=>{ const v=t.meta[k], low=/^(P[3-4]|low|minor|trivial|normal|medium)$/i.test(v); const parts=(t.links&&t.links[k])||!data.options.links[k]&&!data.options.links['*']?[v]:v.split(/,\s+/); const nodes=[]; parts.forEach((part,i)=>{ const href=linkFor(k,part); if(i) nodes.push(', '); nodes.push(href? h('a',{href,target:'_blank',rel:'noopener'},part) : h('span',{class:'v'},part)); }); return h('span',{class:'meta '+k+(low?' low':'')}, h('span',{class:'k'},k), ...nodes); })));
|
|
1559
1559
|
const otherAnn=t.annotations.filter(a=>!DIMS.includes(a.type.toLowerCase()) && !(a.type.toLowerCase() in t.meta));
|
|
1560
1560
|
if(otherAnn.length) d.append(h('h4',{},'Annotations'), h('dl',{class:'kv'}, otherAnn.map(a=>[h('dt',{},a.type),h('dd',{},a.description||'')])));
|
|
1561
1561
|
if(t.results.length>1){
|
package/dist/types.d.ts
CHANGED
|
@@ -77,8 +77,14 @@ export interface ReportingLabsOptions {
|
|
|
77
77
|
/**
|
|
78
78
|
* Turn meta values into links. Key = meta key (story, epic, ticket, issue, jira...), value = URL template with {id}.
|
|
79
79
|
* e.g. { story: 'https://acme.atlassian.net/browse/{id}', epic: 'https://acme.atlassian.net/browse/{id}' }
|
|
80
|
+
*
|
|
81
|
+
* A link that needs more than the displayed value takes an object. Placeholders name the fields of the
|
|
82
|
+
* object passed to meta(); `display` (default '{id}') is what the report shows, the rest only builds the URL:
|
|
83
|
+
* links: { octaneTestCase: { url: 'https://oss.valueedge.com/ui/?p={p}#/entity-navigation?entityType=test&id={id}', display: '{id}' } }
|
|
84
|
+
* meta({ octaneTestCase: { id: '58966', p: '4001/14014' } })
|
|
85
|
+
* The report shows "octaneTestCase 58966", clicking it opens the full URL; p is never shown.
|
|
80
86
|
*/
|
|
81
|
-
links?: Record<string, string>;
|
|
87
|
+
links?: Record<string, string | LinkTemplate>;
|
|
82
88
|
/** Keys (case-insensitive substrings) whose values are masked in test data and API panels. */
|
|
83
89
|
maskKeys?: string[];
|
|
84
90
|
/** Keep a rolling run history next to the report and draw a trend chart. Per-test outcomes are stored too, which powers new-vs-known failures, flaky history and duration regressions. */
|
|
@@ -94,6 +100,13 @@ export interface ReportingLabsOptions {
|
|
|
94
100
|
/** Style Given/When/Then steps as Gherkin and label describe blocks as Features/Scenarios. Default: auto-detect */
|
|
95
101
|
bdd?: boolean;
|
|
96
102
|
}
|
|
103
|
+
/** A link built from several fields of a meta() object. */
|
|
104
|
+
export interface LinkTemplate {
|
|
105
|
+
/** URL with {field} placeholders, e.g. 'https://tms.example.com/{project}/cases/{id}'. */
|
|
106
|
+
url: string;
|
|
107
|
+
/** What the report shows for this meta key, with the same placeholders. Default '{id}'. */
|
|
108
|
+
display?: string;
|
|
109
|
+
}
|
|
97
110
|
/** Lets you type a known value and still accept any string. */
|
|
98
111
|
export type Loose<T extends string> = T | (string & {});
|
|
99
112
|
/**
|
|
@@ -114,14 +127,14 @@ export interface TestMeta extends CustomMeta {
|
|
|
114
127
|
feature?: string;
|
|
115
128
|
/** Epic key, e.g. 'EPIC-18'. Becomes a link when `links.epic` is configured. */
|
|
116
129
|
epic?: string;
|
|
117
|
-
/** User story key, e.g. 'SHOP-250'. Becomes a link when `links.story` is configured. */
|
|
118
|
-
story?: string;
|
|
119
|
-
/** Bug / issue key this test guards against, e.g. 'PROMO-118'. */
|
|
120
|
-
issue?: string;
|
|
130
|
+
/** User story key, e.g. 'SHOP-250', or several. Becomes a link when `links.story` is configured. */
|
|
131
|
+
story?: string | string[];
|
|
132
|
+
/** Bug / issue key this test guards against, e.g. 'PROMO-118', or several. */
|
|
133
|
+
issue?: string | string[];
|
|
121
134
|
/** Test case id in your TCM tool (TestRail, Xray, Zephyr...). */
|
|
122
135
|
testCaseId?: string;
|
|
123
|
-
/** Free-form tags or any other key you like. */
|
|
124
|
-
[key: string]: string | number | undefined;
|
|
136
|
+
/** Free-form tags or any other key you like. An object holds the fields of a multi-parameter link, see `links`. */
|
|
137
|
+
[key: string]: string | number | (string | number)[] | Record<string, string | number> | undefined;
|
|
125
138
|
}
|
|
126
139
|
/** Empty by default; augment it to add typed keys of your own. */
|
|
127
140
|
export interface CustomMeta {
|
|
@@ -237,6 +250,8 @@ export interface TestData {
|
|
|
237
250
|
}[];
|
|
238
251
|
/** extracted dimension values, e.g. { priority: 'P1', severity: 'critical' } */
|
|
239
252
|
meta: Record<string, string>;
|
|
253
|
+
/** Ready-made hrefs for meta keys whose link needed more than the displayed value (object meta + LinkTemplate). */
|
|
254
|
+
links?: Record<string, string>;
|
|
240
255
|
outcome: Status;
|
|
241
256
|
duration: number;
|
|
242
257
|
results: ResultData[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|