reporting-labs 0.6.9 → 0.6.11

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,203 @@
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.reportingLabsComplete = reportingLabsComplete;
37
+ /**
38
+ * Combines the per-runner partials written by the WDIO reporter into the one report —
39
+ * index.html, report.json and (best-effort) report.pdf. Call it from `onComplete` in wdio.conf:
40
+ *
41
+ * import { reportingLabsComplete } from 'reporting-labs/wdio';
42
+ * export const config = {
43
+ * reporters: [[ReportingLabsWdioReporter, { outputFolder: 'reporting-labs' }]],
44
+ * async onComplete() { await reportingLabsComplete({ outputFolder: 'reporting-labs', title: 'My suite' }); },
45
+ * };
46
+ */
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const template_1 = require("../template");
50
+ const mask_1 = require("../mask");
51
+ const env_1 = require("./env");
52
+ async function reportingLabsComplete(options = {}) {
53
+ const outputFolder = options.outputFolder || 'reporting-labs';
54
+ const partsDir = path.join(outputFolder, '.rl-wdio');
55
+ if (!fs.existsSync(partsDir))
56
+ return undefined;
57
+ const files = fs.readdirSync(partsDir).filter(f => f.endsWith('.json'));
58
+ if (!files.length)
59
+ return undefined;
60
+ const masker = (0, mask_1.makeMasker)(options.maskKeys || [], { knownValues: options.maskValues || [], fromEnv: options.maskFromEnv !== false });
61
+ const tests = [];
62
+ let startTime = Number.MAX_SAFE_INTEGER;
63
+ let endTime = 0;
64
+ let runStatus = 'passed';
65
+ const workers = files.length;
66
+ for (const f of files) {
67
+ let part;
68
+ try {
69
+ part = JSON.parse(fs.readFileSync(path.join(partsDir, f), 'utf8'));
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ if (typeof part.startTime === 'number')
75
+ startTime = Math.min(startTime, part.startTime);
76
+ if (typeof part.endTime === 'number')
77
+ endTime = Math.max(endTime, part.endTime);
78
+ if (part.runStatus === 'failed')
79
+ runStatus = 'failed';
80
+ for (const t of part.tests || [])
81
+ tests.push(t);
82
+ }
83
+ if (startTime === Number.MAX_SAFE_INTEGER)
84
+ startTime = Date.now();
85
+ if (!endTime)
86
+ endTime = Date.now();
87
+ const stats = { passed: 0, failed: 0, skipped: 0, flaky: 0, timedOut: 0, interrupted: 0, total: tests.length };
88
+ for (const t of tests)
89
+ stats[t.outcome]++;
90
+ const projects = [...new Set(tests.map(t => t.project).filter(Boolean))];
91
+ const base = process.cwd();
92
+ const dims = (options.dimensions ?? ['priority', 'severity', 'feature', 'owner']).map(d => d.toLowerCase());
93
+ const histOn = options.history?.enabled ?? true;
94
+ const histFile = path.resolve(base, options.history?.file ?? 'reporting-labs.history.json');
95
+ let entries = [];
96
+ if (histOn) {
97
+ try {
98
+ entries = JSON.parse(fs.readFileSync(histFile, 'utf8'));
99
+ }
100
+ catch { /* first run */ }
101
+ }
102
+ const failedCount = stats.failed + stats.timedOut + stats.interrupted;
103
+ const code = (o) => o === 'passed' ? 'p' : o === 'flaky' ? 'k' : o === 'skipped' ? 's' : 'f';
104
+ const perTest = {};
105
+ for (const t of tests) {
106
+ const last = t.results[t.results.length - 1];
107
+ perTest[t.key] = [code(t.outcome), Math.round(last?.duration ?? t.duration)];
108
+ }
109
+ const current = {
110
+ time: startTime, duration: endTime - startTime,
111
+ passed: stats.passed, failed: failedCount, flaky: stats.flaky, skipped: stats.skipped, total: stats.total,
112
+ label: options.metadata?.build ?? (0, env_1.ciRunLabel)(process.env) ?? options.metadata?.branch, tests: perTest,
113
+ };
114
+ const history = [...entries, current].slice(-(options.history?.keep ?? 30));
115
+ if (histOn) {
116
+ try {
117
+ fs.writeFileSync(histFile, JSON.stringify(history, null, 1));
118
+ }
119
+ catch { /* read-only fs */ }
120
+ }
121
+ const metadata = Object.fromEntries(Object.entries(options.metadata ?? {}).map(([k, v]) => [k, masker.maskStr(String(v))]));
122
+ const bdd = options.bdd ?? tests.some(t => t.results.some(r => r.steps.some(st => /^(Given|When|Then|And|But)\b/.test(st.title))));
123
+ const data = {
124
+ title: masker.maskStr(options.title ?? 'Test report'),
125
+ generatedAt: Date.now(),
126
+ startTime,
127
+ duration: endTime - startTime,
128
+ metadata,
129
+ projects,
130
+ workers,
131
+ stats,
132
+ tests,
133
+ history,
134
+ bdd,
135
+ rootDir: base,
136
+ env: (0, env_1.collectEnv)(base, Object.fromEntries(Object.entries(options.env ?? {}).map(([k, v]) => [k, masker.maskStr(String(v))])), [...new Set(projects)], workers, options.metadata?.branch).map(r => ({ ...r, v: masker.maskStr(r.v) })),
137
+ runStatus,
138
+ globalErrors: [],
139
+ globalOutput: [],
140
+ options: {
141
+ logo: options.logo,
142
+ accent: options.accent,
143
+ theme: options.theme ?? 'auto',
144
+ palette: options.palette ?? 'lab',
145
+ embedFonts: options.embedFonts ?? true,
146
+ sections: options.sections ?? [],
147
+ widgets: { runStrip: true, outcome: true, attention: true, dimensions: true, timeline: true, durations: true, tags: true, slowest: true, projects: true, flaky: true, environment: true, skipped: true },
148
+ dimensions: dims,
149
+ dimensionOrder: {
150
+ priority: ['P0', 'P1', 'P2', 'P3', 'P4'],
151
+ severity: ['blocker', 'critical', 'major', 'high', 'medium', 'normal', 'minor', 'low', 'trivial'],
152
+ ...(options.dimensionOrder ?? {}),
153
+ },
154
+ project: options.project,
155
+ links: Object.fromEntries(Object.entries(options.links ?? {}).map(([k, v]) => [k.toLowerCase(), typeof v === 'string' ? v : v.url])),
156
+ customCss: options.customCss ?? '',
157
+ editorLinks: options.editorLinks ?? !process.env.CI,
158
+ },
159
+ };
160
+ fs.mkdirSync(outputFolder, { recursive: true });
161
+ const htmlFile = path.join(outputFolder, options.outputFile ?? 'index.html');
162
+ fs.writeFileSync(htmlFile, (0, template_1.renderHtml)(data), 'utf8');
163
+ if (options.emitJson !== false)
164
+ fs.writeFileSync(path.join(outputFolder, options.jsonFile ?? 'report.json'), JSON.stringify(data), 'utf8');
165
+ // tidy the partials
166
+ try {
167
+ fs.rmSync(partsDir, { recursive: true, force: true });
168
+ }
169
+ catch { /* leave them */ }
170
+ if (options.announce !== false) {
171
+ console.log(`\n reporting-labs: report written to ${path.relative(process.cwd(), htmlFile)}`);
172
+ }
173
+ if (options.pdf !== false)
174
+ await writePdf(htmlFile, outputFolder, options);
175
+ if (options.announce !== false)
176
+ console.log('');
177
+ return htmlFile;
178
+ }
179
+ /** Render the report's print layout to a PDF with the Chromium Playwright ships. Best-effort. */
180
+ async function writePdf(htmlFile, outputFolder, options) {
181
+ const name = (typeof options.pdf === 'object' && options.pdf?.file) || 'report.pdf';
182
+ const pdfFile = path.join(outputFolder, name);
183
+ try {
184
+ const { chromium } = await import('@playwright/test');
185
+ const { pathToFileURL } = await import('url');
186
+ const browser = await chromium.launch();
187
+ try {
188
+ const page = await browser.newPage();
189
+ await page.goto(pathToFileURL(htmlFile).href, { waitUntil: 'load' });
190
+ await page.evaluate(() => window.reportingLabsPreparePrint?.());
191
+ await page.pdf({ path: pdfFile, printBackground: true, preferCSSPageSize: true });
192
+ }
193
+ finally {
194
+ await browser.close();
195
+ }
196
+ if (options.announce !== false)
197
+ console.log(` reporting-labs: PDF written to ${path.relative(process.cwd(), pdfFile)}`);
198
+ }
199
+ catch (e) {
200
+ if (options.announce !== false)
201
+ console.log(` reporting-labs: PDF skipped — ${String(e.message).split('\n')[0]}`);
202
+ }
203
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * reportingLabs for WebdriverIO.
3
+ *
4
+ * // wdio.conf.ts
5
+ * import ReportingLabsReporter, { reportingLabsComplete } from 'reporting-labs/wdio';
6
+ *
7
+ * export const config = {
8
+ * reporters: [[ReportingLabsReporter, { outputFolder: 'reporting-labs' }]],
9
+ * async onComplete() { await reportingLabsComplete({ outputFolder: 'reporting-labs', title: 'My suite' }); },
10
+ * };
11
+ */
12
+ import ReportingLabsWdioReporter from './reporter';
13
+ export { ReportingLabsWdioReporter };
14
+ export type { ReportingLabsWdioOptions } from './reporter';
15
+ export { reportingLabsComplete } from './finalize';
16
+ export type { ReportingLabsCompleteOptions } from './finalize';
17
+ export default ReportingLabsWdioReporter;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.reportingLabsComplete = exports.ReportingLabsWdioReporter = void 0;
7
+ /**
8
+ * reportingLabs for WebdriverIO.
9
+ *
10
+ * // wdio.conf.ts
11
+ * import ReportingLabsReporter, { reportingLabsComplete } from 'reporting-labs/wdio';
12
+ *
13
+ * export const config = {
14
+ * reporters: [[ReportingLabsReporter, { outputFolder: 'reporting-labs' }]],
15
+ * async onComplete() { await reportingLabsComplete({ outputFolder: 'reporting-labs', title: 'My suite' }); },
16
+ * };
17
+ */
18
+ const reporter_1 = __importDefault(require("./reporter"));
19
+ exports.ReportingLabsWdioReporter = reporter_1.default;
20
+ var finalize_1 = require("./finalize");
21
+ Object.defineProperty(exports, "reportingLabsComplete", { enumerable: true, get: function () { return finalize_1.reportingLabsComplete; } });
22
+ exports.default = reporter_1.default;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * reportingLabs reporter for WebdriverIO.
3
+ *
4
+ * WDIO drives a reporter by emitting lifecycle events onto it and reading `isSynchronised`, so this
5
+ * class is a plain EventEmitter — it needs no `@wdio/reporter` at runtime and stays CommonJS, which
6
+ * loads on every Node version. One instance runs per runner (per spec / worker); it collects that
7
+ * runner's suites, tests, WebDriver-command steps, errors and (on failure) a screenshot, then writes
8
+ * a partial JSON. `reportingLabsComplete()` (a one-line `onComplete` hook) combines every partial into
9
+ * the one report — the same index.html, report.json and report.pdf the Playwright reporter produces.
10
+ *
11
+ * Nothing here touches the Playwright reporter; it only reuses the shared, pure helpers.
12
+ */
13
+ import { EventEmitter } from 'node:events';
14
+ declare const META_KEYS: string[];
15
+ export interface ReportingLabsWdioOptions {
16
+ outputFolder?: string;
17
+ title?: string;
18
+ screenshot?: 'on-failure' | 'always' | 'off';
19
+ maskKeys?: string[];
20
+ maskValues?: string[];
21
+ maskFromEnv?: boolean;
22
+ [k: string]: unknown;
23
+ }
24
+ export default class ReportingLabsWdioReporter extends EventEmitter {
25
+ options: ReportingLabsWdioOptions;
26
+ outputStream: {
27
+ write: (_c: unknown) => boolean;
28
+ };
29
+ private masker;
30
+ private startTime;
31
+ private suiteStack;
32
+ private rlTests;
33
+ private specs;
34
+ private cur?;
35
+ private elTypes;
36
+ private cid;
37
+ private capsName;
38
+ private runStatus;
39
+ private pending;
40
+ private endInfo?;
41
+ private written;
42
+ constructor(options?: ReportingLabsWdioOptions);
43
+ /** WDIO polls this before tearing a worker down; we hold it open until the partial is written. */
44
+ get isSynchronised(): boolean;
45
+ write(_content: unknown): void;
46
+ private safe;
47
+ private onRunnerStart;
48
+ private onSuiteStart;
49
+ private onSuiteEnd;
50
+ private onTestStart;
51
+ private onBeforeCommand;
52
+ private onAfterCommand;
53
+ private endTest;
54
+ /** Best-effort screenshot of the live session on failure. Async — gated by isSynchronised so the
55
+ * partial is written only once it resolves. */
56
+ private maybeShot;
57
+ private toError;
58
+ private onRunnerEnd;
59
+ private tryWrite;
60
+ private rel;
61
+ }
62
+ /** Pull meta (priority/severity/owner/...) from WDIO tags like `@priority:P1`, `@P1`, `@critical`. */
63
+ export declare function metaFromTags(tags: string[], dims: string[]): {
64
+ meta: Record<string, string>;
65
+ clean: string[];
66
+ };
67
+ export { META_KEYS };
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "reporting-labs",
3
- "version": "0.6.9",
4
- "description": "reportingLabs – beautiful, customizable single-file HTML test reports. One line of config. Playwright adapter included, more runners on the way.",
3
+ "version": "0.6.11",
4
+ "description": "reportingLabs \u2013 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",
7
7
  "html-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
  }