reporting-labs 0.6.10 → 0.6.12

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.
@@ -0,0 +1,332 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.META_KEYS = void 0;
37
+ exports.metaFromTags = metaFromTags;
38
+ /**
39
+ * reportingLabs reporter for WebdriverIO.
40
+ *
41
+ * WDIO drives a reporter by emitting lifecycle events onto it and reading `isSynchronised`, so this
42
+ * class is a plain EventEmitter — it needs no `@wdio/reporter` at runtime and stays CommonJS, which
43
+ * loads on every Node version. One instance runs per runner (per spec / worker); it collects that
44
+ * runner's suites, tests, WebDriver-command steps, errors and (on failure) a screenshot, then writes
45
+ * a partial JSON. `reportingLabsComplete()` (a one-line `onComplete` hook) combines every partial into
46
+ * the one report — the same index.html, report.json and report.pdf the Playwright reporter produces.
47
+ *
48
+ * Nothing here touches the Playwright reporter; it only reuses the shared, pure helpers.
49
+ */
50
+ const node_events_1 = require("node:events");
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const mask_1 = require("../mask");
54
+ const explain_1 = require("../explain");
55
+ const META_KEYS = ['priority', 'severity', 'feature', 'owner', 'epic', 'story', 'issue', 'bug', 'component', 'module', 'team', 'sprint', 'testcase', 'tms', 'requirement'];
56
+ exports.META_KEYS = META_KEYS;
57
+ /** WebDriver command name → readable step verb. Commands not listed (reads, internals) are not steps. */
58
+ const CMD = {
59
+ navigateTo: 'open', url: 'open', back: 'back', forward: 'forward', refresh: 'refresh',
60
+ elementClick: 'click', click: 'click', doubleClick: 'double click',
61
+ elementSendKeys: 'set value', setValue: 'set value', addValue: 'add value', elementClear: 'clear', clearValue: 'clear',
62
+ findElement: 'find', findElements: 'find all', findElementFromElement: 'find', findElementsFromElement: 'find all',
63
+ switchToFrame: 'switch to frame', switchToParentFrame: 'switch to parent frame', switchToWindow: 'switch to window',
64
+ acceptAlert: 'accept alert', dismissAlert: 'dismiss alert', sendAlertText: 'type into alert',
65
+ performActions: 'perform actions', executeScript: 'execute script', executeAsyncScript: 'execute async script',
66
+ setWindowRect: 'resize window', maximizeWindow: 'maximize window', minimizeWindow: 'minimize window', fullscreenWindow: 'fullscreen window',
67
+ uploadFile: 'upload file', addCookie: 'add cookie', deleteCookie: 'delete cookie', deleteAllCookies: 'delete all cookies',
68
+ };
69
+ class ReportingLabsWdioReporter extends node_events_1.EventEmitter {
70
+ constructor(options = {}) {
71
+ super();
72
+ this.outputStream = { write: (_c) => true };
73
+ this.startTime = Date.now();
74
+ this.suiteStack = [];
75
+ this.rlTests = [];
76
+ this.specs = [];
77
+ this.elTypes = {};
78
+ this.cid = '';
79
+ this.capsName = '';
80
+ this.runStatus = 'passed';
81
+ this.pending = 0;
82
+ this.written = false;
83
+ this.setMaxListeners(0);
84
+ this.options = options || {};
85
+ this.masker = (0, mask_1.makeMasker)(this.options.maskKeys || [], { knownValues: this.options.maskValues || [], fromEnv: this.options.maskFromEnv !== false });
86
+ this.on('runner:start', (r) => this.safe(() => this.onRunnerStart(r)));
87
+ this.on('suite:start', (s) => this.safe(() => this.onSuiteStart(s)));
88
+ this.on('suite:end', () => this.safe(() => this.onSuiteEnd()));
89
+ this.on('test:start', (t) => this.safe(() => this.onTestStart(t)));
90
+ this.on('test:pass', (t) => this.safe(() => this.endTest(t, 'passed')));
91
+ this.on('test:fail', (t) => this.safe(() => { this.runStatus = 'failed'; this.endTest(t, 'failed'); }));
92
+ this.on('test:skip', (t) => this.safe(() => this.endTest(t, 'skipped', t.pendingReason)));
93
+ this.on('test:pending', (t) => this.safe(() => this.endTest(t, 'skipped', t.pendingReason)));
94
+ this.on('client:beforeCommand', (c) => this.safe(() => this.onBeforeCommand(c)));
95
+ this.on('client:afterCommand', (c) => this.safe(() => this.onAfterCommand(c)));
96
+ this.on('runner:end', () => this.safe(() => this.onRunnerEnd()));
97
+ }
98
+ /** WDIO polls this before tearing a worker down; we hold it open until the partial is written. */
99
+ get isSynchronised() {
100
+ return this.written || (this.pending === 0 && !this.endInfo);
101
+ }
102
+ write(_content) { }
103
+ safe(fn) { try {
104
+ fn();
105
+ }
106
+ catch { /* a reporter bug must never fail the run */ } }
107
+ onRunnerStart(runner) {
108
+ this.startTime = Date.now();
109
+ this.cid = runner.cid || '';
110
+ this.specs = runner.specs || [];
111
+ this.capsName = capsName(runner.capabilities);
112
+ }
113
+ onSuiteStart(suite) {
114
+ if (suite.title)
115
+ this.suiteStack.push(suite.title);
116
+ }
117
+ onSuiteEnd() { this.suiteStack.pop(); }
118
+ onTestStart(test) {
119
+ const now = Date.now();
120
+ const result = {
121
+ retry: test.retries || 0, status: 'passed', duration: 0, startTime: now, workerIndex: numericCid(this.cid),
122
+ errors: [], steps: [], attachments: [], stdout: [], stderr: [], logs: [], data: [], api: [],
123
+ };
124
+ const file = this.rel(test.file || this.specs[0] || '');
125
+ const tagSource = [test.title || '', ...this.suiteStack].join(' ');
126
+ const tags = (tagSource.match(/@[A-Za-z][\w:.=-]*/g) || []);
127
+ const { meta } = metaFromTags(tags, META_KEYS);
128
+ const t = {
129
+ id: test.uid || `${file}:${test.title}`,
130
+ key: '', title: this.masker.maskStr(test.title || ''), path: this.suiteStack.map(s => this.masker.maskStr(s)),
131
+ file, line: 0, project: this.capsName, tags, annotations: [], meta,
132
+ outcome: 'passed', duration: 0, results: [result], retries: test.retries,
133
+ };
134
+ t.key = `${t.project}::${t.file}::${[...t.path, t.title].join(' › ')}`;
135
+ this.cur = { test: t, result, stepStack: [] };
136
+ }
137
+ onBeforeCommand(cmd) {
138
+ if (!this.cur)
139
+ return;
140
+ // a value typed into a password / secret field is masked: WDIO reads the element's `type` property
141
+ // right before typing, so look up what we learned for this element id.
142
+ const fieldType = this.elTypes[elementId(cmd.endpoint)] || '';
143
+ const title = cmdTitle(cmd, this.masker, fieldType);
144
+ if (!title)
145
+ return;
146
+ const step = { title, category: 'wdio', duration: 0, steps: [] };
147
+ step._start = Date.now();
148
+ const parent = this.cur.stepStack[this.cur.stepStack.length - 1];
149
+ (parent ? parent.steps : this.cur.result.steps).push(step);
150
+ this.cur.stepStack.push(step);
151
+ }
152
+ onAfterCommand(cmd) {
153
+ // record element input types so the next sendKeys to that element can be masked
154
+ if (cmd && cmd.command === 'getElementProperty' && /\/property\/type$/.test(cmd.endpoint || '')) {
155
+ const id = elementId(cmd.endpoint);
156
+ const v = cmd.result?.value;
157
+ if (id && typeof v === 'string')
158
+ this.elTypes[id] = v;
159
+ }
160
+ if (!this.cur)
161
+ return;
162
+ const step = this.cur.stepStack.pop();
163
+ if (step) {
164
+ step.duration = Date.now() - (step._start || Date.now());
165
+ delete step._start;
166
+ }
167
+ }
168
+ endTest(test, status, skipReason) {
169
+ if (!this.cur)
170
+ this.onTestStart(test); // a skipped / pending test may never fire test:start
171
+ if (!this.cur)
172
+ return;
173
+ const { test: t, result } = this.cur;
174
+ this.cur.stepStack.length = 0;
175
+ result.status = status;
176
+ result.duration = test.duration ?? (Date.now() - result.startTime);
177
+ let outcome = status;
178
+ if (status === 'passed' && (test.retries || 0) > 0)
179
+ outcome = 'flaky';
180
+ t.outcome = outcome;
181
+ t.duration = result.duration;
182
+ if (skipReason) {
183
+ t.note = skipReason;
184
+ t.annotations.push({ type: 'skip', description: skipReason });
185
+ }
186
+ if (status === 'failed') {
187
+ const errs = (test.errors && test.errors.length ? test.errors : (test.error ? [test.error] : []));
188
+ for (const e of errs)
189
+ result.errors.push(this.toError(e));
190
+ this.maybeShot(result);
191
+ }
192
+ this.rlTests.push(t);
193
+ this.cur = undefined;
194
+ }
195
+ /** Best-effort screenshot of the live session on failure. Async — gated by isSynchronised so the
196
+ * partial is written only once it resolves. */
197
+ maybeShot(result) {
198
+ if (this.options.screenshot === 'off')
199
+ return;
200
+ const b = global.browser;
201
+ if (!b || typeof b.takeScreenshot !== 'function')
202
+ return;
203
+ this.pending++;
204
+ Promise.resolve()
205
+ .then(() => b.takeScreenshot())
206
+ .then((b64) => { if (b64)
207
+ result.attachments.push({ name: 'screenshot', contentType: 'image/png', src: 'data:image/png;base64,' + b64 }); })
208
+ .catch(() => { })
209
+ .finally(() => { this.pending--; this.tryWrite(); });
210
+ }
211
+ toError(e) {
212
+ const message = this.masker.maskStr(stripAnsi(String(e?.message ?? e ?? '')));
213
+ const stack = e?.stack ? this.masker.maskStr(stripAnsi(String(e.stack))) : undefined;
214
+ const d = { message };
215
+ const ex = (0, explain_1.explainError)(message);
216
+ if (ex)
217
+ d.explain = ex;
218
+ if (stack && stack !== message)
219
+ d.stack = stack;
220
+ return d;
221
+ }
222
+ onRunnerEnd() {
223
+ this.endInfo = { endTime: Date.now() };
224
+ this.tryWrite();
225
+ }
226
+ tryWrite() {
227
+ if (!this.endInfo || this.pending > 0 || this.written)
228
+ return;
229
+ this.written = true;
230
+ try {
231
+ const dir = path.join(this.options.outputFolder || 'reporting-labs', '.rl-wdio');
232
+ fs.mkdirSync(dir, { recursive: true });
233
+ const part = { startTime: this.startTime, endTime: this.endInfo.endTime, runStatus: this.runStatus, tests: this.rlTests };
234
+ const name = `${(this.cid || 'runner').replace(/[^a-z0-9_-]/gi, '_')}-${process.pid}.json`;
235
+ fs.writeFileSync(path.join(dir, name), JSON.stringify(part));
236
+ }
237
+ catch { /* best-effort; finalize will simply see fewer parts */ }
238
+ }
239
+ rel(file) {
240
+ if (!file)
241
+ return file;
242
+ try {
243
+ return path.relative(process.cwd(), file).split(path.sep).join('/');
244
+ }
245
+ catch {
246
+ return file;
247
+ }
248
+ }
249
+ }
250
+ exports.default = ReportingLabsWdioReporter;
251
+ function numericCid(cid) {
252
+ const m = /^(\d+)/.exec(cid || '');
253
+ return m ? parseInt(m[1], 10) : 0;
254
+ }
255
+ function capsName(caps) {
256
+ if (!caps)
257
+ return '';
258
+ const name = (caps.browserName || caps['appium:deviceName'] || caps.platformName || '');
259
+ const ver = (caps.browserVersion || caps.version || '');
260
+ return [name, ver].filter(Boolean).join(' ').trim();
261
+ }
262
+ function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
263
+ function elementId(endpoint) {
264
+ const m = /\/element\/([^/]+)\//.exec(endpoint || '');
265
+ return m ? m[1] : '';
266
+ }
267
+ function cmdTitle(cmd, masker, fieldType = '') {
268
+ const name = cmd.command || endpointCommand(cmd.endpoint);
269
+ if (!name)
270
+ return null;
271
+ const verb = CMD[name];
272
+ if (!verb)
273
+ return null;
274
+ const body = (cmd.body || {});
275
+ if (name === 'navigateTo' || name === 'url')
276
+ return `open ${short(String(body.url ?? ''))}`;
277
+ if (name === 'findElement' || name === 'findElements' || name === 'findElementFromElement' || name === 'findElementsFromElement') {
278
+ return `${verb} ${String(body.using ?? '')}: ${short(String(body.value ?? ''))}`.trim();
279
+ }
280
+ if (name === 'elementSendKeys' || name === 'setValue' || name === 'addValue') {
281
+ let text = Array.isArray(body.text) ? body.text.join('') : String(body.text ?? body.value ?? '');
282
+ text = /password|hidden/i.test(fieldType) ? '****' : masker.maskStr(text);
283
+ return `${verb} "${short(text)}"`;
284
+ }
285
+ if (name === 'executeScript' || name === 'executeAsyncScript') {
286
+ return `${verb} ${short(String(body.script ?? '').replace(/\s+/g, ' '))}`;
287
+ }
288
+ if (name === 'sendAlertText')
289
+ return 'type into alert';
290
+ return verb;
291
+ }
292
+ function endpointCommand(endpoint) {
293
+ if (!endpoint)
294
+ return null;
295
+ if (/\/url$/.test(endpoint))
296
+ return 'navigateTo';
297
+ if (/\/click$/.test(endpoint))
298
+ return 'elementClick';
299
+ if (/\/value$/.test(endpoint))
300
+ return 'elementSendKeys';
301
+ if (/\/clear$/.test(endpoint))
302
+ return 'elementClear';
303
+ if (/\/elements$/.test(endpoint))
304
+ return 'findElements';
305
+ if (/\/element$/.test(endpoint))
306
+ return 'findElement';
307
+ return null;
308
+ }
309
+ function short(s, max = 80) { return s.length <= max ? s : s.slice(0, max - 1) + '…'; }
310
+ /** Pull meta (priority/severity/owner/...) from WDIO tags like `@priority:P1`, `@P1`, `@critical`. */
311
+ function metaFromTags(tags, dims) {
312
+ const meta = {};
313
+ const clean = [];
314
+ for (const raw of tags) {
315
+ const tag = String(raw).replace(/^@/, '');
316
+ const m = /^([a-z_-]+)[:=](.+)$/i.exec(tag);
317
+ if (m && dims.includes(m[1].toLowerCase())) {
318
+ meta[m[1].toLowerCase()] = m[2];
319
+ continue;
320
+ }
321
+ if (/^P[0-4]$/i.test(tag) && dims.includes('priority') && !meta.priority) {
322
+ meta.priority = tag.toUpperCase();
323
+ continue;
324
+ }
325
+ if (/^(blocker|critical|major|minor|trivial)$/i.test(tag) && dims.includes('severity') && !meta.severity) {
326
+ meta.severity = tag.toLowerCase();
327
+ continue;
328
+ }
329
+ clean.push(raw);
330
+ }
331
+ return { meta, clean };
332
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reporting-labs",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
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",
@@ -29,6 +29,10 @@
29
29
  "./auto": {
30
30
  "types": "./dist/auto.d.ts",
31
31
  "default": "./dist/auto.js"
32
+ },
33
+ "./wdio": {
34
+ "types": "./dist/wdio/index.d.ts",
35
+ "default": "./dist/wdio/index.js"
32
36
  }
33
37
  },
34
38
  "bin": {
@@ -69,5 +73,10 @@
69
73
  },
70
74
  "engines": {
71
75
  "node": ">=18"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@playwright/test": {
79
+ "optional": true
80
+ }
72
81
  }
73
82
  }