testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn

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.
Files changed (67) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.js +6 -26
  3. package/package.json +39 -4
  4. package/src/adapter/codecept.js +626 -0
  5. package/src/adapter/cucumber/current.js +230 -0
  6. package/src/adapter/cucumber/legacy.js +158 -0
  7. package/src/adapter/cucumber.js +4 -0
  8. package/src/adapter/cypress-plugin/index.js +110 -0
  9. package/src/adapter/jasmine.js +60 -0
  10. package/src/adapter/jest.js +108 -0
  11. package/src/adapter/mocha.cjs +2 -0
  12. package/src/adapter/mocha.js +211 -0
  13. package/src/adapter/nightwatch.js +88 -0
  14. package/src/adapter/playwright.js +343 -0
  15. package/src/adapter/utils/playwright.js +121 -0
  16. package/src/adapter/utils/step-formatter.js +232 -0
  17. package/src/adapter/vitest.js +455 -0
  18. package/src/adapter/webdriver.js +201 -0
  19. package/src/bin/cli.js +507 -0
  20. package/src/bin/reportXml.js +79 -0
  21. package/src/bin/startTest.js +54 -0
  22. package/src/bin/uploadArtifacts.js +91 -0
  23. package/src/client.js +524 -0
  24. package/src/config.js +30 -0
  25. package/src/constants.js +72 -0
  26. package/src/data-storage.js +204 -0
  27. package/src/helpers.js +1 -0
  28. package/src/junit-adapter/adapter.js +23 -0
  29. package/src/junit-adapter/csharp.js +70 -0
  30. package/src/junit-adapter/index.js +28 -0
  31. package/src/junit-adapter/java.js +58 -0
  32. package/src/junit-adapter/javascript.js +31 -0
  33. package/src/junit-adapter/nunit-parser.js +474 -0
  34. package/src/junit-adapter/python.js +42 -0
  35. package/src/junit-adapter/ruby.js +10 -0
  36. package/src/output.js +57 -0
  37. package/src/pipe/bitbucket.js +285 -0
  38. package/src/pipe/coverage.js +500 -0
  39. package/src/pipe/csv.js +161 -0
  40. package/src/pipe/debug.js +143 -0
  41. package/src/pipe/github.js +256 -0
  42. package/src/pipe/gitlab.js +258 -0
  43. package/src/pipe/html.js +1153 -0
  44. package/src/pipe/index.js +73 -0
  45. package/src/pipe/markdown.js +753 -0
  46. package/src/pipe/testomatio.js +707 -0
  47. package/src/replay.js +274 -0
  48. package/src/reporter-functions.js +155 -0
  49. package/src/reporter.js +42 -0
  50. package/src/services/artifacts.js +59 -0
  51. package/src/services/index.js +15 -0
  52. package/src/services/key-values.js +59 -0
  53. package/src/services/links.js +69 -0
  54. package/src/services/logger.js +320 -0
  55. package/src/template/emptyData.svg +23 -0
  56. package/src/template/testomatio-old.hbs +1421 -0
  57. package/src/template/testomatio.hbs +3726 -0
  58. package/src/uploader.js +382 -0
  59. package/src/utils/constants.js +12 -0
  60. package/src/utils/debug.js +20 -0
  61. package/src/utils/log-formatter.js +118 -0
  62. package/src/utils/log.js +88 -0
  63. package/src/utils/pipe_utils.js +193 -0
  64. package/src/utils/utils.js +732 -0
  65. package/src/xmlReader.js +834 -0
  66. package/types/types.d.ts +425 -0
  67. package/types/vitest.types.d.ts +93 -0
@@ -0,0 +1,455 @@
1
+ import pc from 'picocolors';
2
+ import { Client as TestomatioClient } from '../client.js';
3
+ import { STATUS } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
+ import createDebugMessages from 'debug';
6
+
7
+ const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
8
+
9
+ /**
10
+ * @typedef {import('../../types/types.js').VitestTest} VitestTest
11
+ * @typedef {import('../../types/types.js').VitestTestFile} VitestTestFile
12
+ * @typedef {import('../../types/types.js').VitestSuite} VitestSuite
13
+ * @typedef {import('../../types/types.js').VitestTestLogs} VitestTestLogs
14
+ * @typedef {import('../../types/vitest.types.js').ErrorWithDiff} ErrorWithDiff
15
+ * @typedef {typeof import('../constants.js').STATUS} STATUS
16
+ * @typedef {import('../../types/types.js').TestData} TestData
17
+ */
18
+
19
+ class VitestReporter {
20
+ constructor(config = {}) {
21
+ this.client = new TestomatioClient({ apiKey: config?.apiKey });
22
+ /** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
23
+ this.tests = [];
24
+ this._finalized = false;
25
+ this._finalizing = false;
26
+ this._runStartedAtMs = null;
27
+ this._runStartedAtMicros = null;
28
+ this._reportedTestKeys = new Set();
29
+ this._liveQueue = Promise.resolve();
30
+ }
31
+
32
+ // on run start
33
+ onInit() {
34
+ const now = Date.now();
35
+ this._finalized = false;
36
+ this._finalizing = false;
37
+ this._runStartedAtMs = now;
38
+ this._runStartedAtMicros = now * 1000;
39
+ this._reportedTestKeys = new Set();
40
+ this._liveQueue = Promise.resolve();
41
+ this.client.createRun();
42
+ }
43
+
44
+ /**
45
+ * Vitest 3/4 callback fired when test run starts.
46
+ */
47
+ onTestRunStart() {
48
+ const now = Date.now();
49
+ this._runStartedAtMs = now;
50
+ this._runStartedAtMicros = now * 1000;
51
+ }
52
+
53
+ /**
54
+ * @param {VitestTestFile[] | undefined} files // array with results;
55
+ * @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
56
+ */
57
+ async onFinished(files, errors) {
58
+ if (this._finalized || this._finalizing) return;
59
+ this._finalizing = true;
60
+
61
+ try {
62
+ this.tests = [];
63
+ if (!files || !files.length) {
64
+ console.info('No tests executed');
65
+ return;
66
+ }
67
+
68
+ files.forEach(file => {
69
+ // task could be test or suite
70
+ getTasks(file).forEach(taskOrSuite => {
71
+ if (taskOrSuite.type === 'test') {
72
+ const test = taskOrSuite;
73
+ this.tests.push(this.#getDataFromTest(test));
74
+ } else if (taskOrSuite.type === 'suite') {
75
+ const suite = taskOrSuite;
76
+ this.#processTasksOfSuite(suite);
77
+ } else {
78
+ throw new Error('Unprocessed case. Unknown task type');
79
+ }
80
+ });
81
+ });
82
+
83
+ debug(this.tests.length, 'tests collected');
84
+
85
+ // send tests to Testomat.io
86
+ for (const test of this.tests) {
87
+ if (test._reportKey && this._reportedTestKeys.has(test._reportKey)) continue;
88
+ if (test._reportKey) this._reportedTestKeys.add(test._reportKey);
89
+ await this.client.addTestRun(test.status, test);
90
+ }
91
+ await this._liveQueue;
92
+
93
+ console.log('finished');
94
+ if (errors.length) console.error('Vitest adapter errors:', errors);
95
+
96
+ const startedAtMs = this._runStartedAtMs || getEarliestTestStartMs(files) || Date.now();
97
+ const duration = Math.max(0, (Date.now() - startedAtMs) / 1000);
98
+ await this.client.updateRunStatus(getRunStatusFromResults(files), { duration });
99
+ this._finalized = true;
100
+ } finally {
101
+ this._finalizing = false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Vitest 4+ reporter API callback.
107
+ *
108
+ * @param {Array<unknown> | undefined} testModules
109
+ * @param {unknown[] | undefined} errors
110
+ */
111
+ async onTestRunEnd(testModules, errors) {
112
+ const files = (testModules || [])
113
+ .map(module => module && (/** @type {any} */ (module).task || module))
114
+ .filter(Boolean);
115
+ await this.onFinished(files, errors);
116
+ }
117
+
118
+ /**
119
+ * Vitest 4 callback fired when single test case is finished.
120
+ *
121
+ * @param {unknown} testCase
122
+ */
123
+ async onTestCaseResult(testCase) {
124
+ await this.#reportLive(testCase);
125
+ }
126
+
127
+ /**
128
+ * Vitest 3 fallback callback with task updates.
129
+ *
130
+ * @param {unknown[] | undefined} packs
131
+ */
132
+ async onTaskUpdate(packs) {
133
+ if (!Array.isArray(packs) || !packs.length) return;
134
+ for (const pack of packs) {
135
+ const test = getTestFromTaskUpdatePack(pack);
136
+ if (test) await this.#reportLive(test);
137
+ }
138
+ }
139
+
140
+ /* non-used listeners
141
+ onUserConsoleLog(log) {}
142
+ onPathsCollected(paths) {} // paths array to files with tests
143
+ onCollected(files) {} // files array with tests (but without results)
144
+ onTaskUpdate(packs) {} // some updates come here on afterAll block execution
145
+ onTestRemoved(trigger) {}
146
+ onWatcherStart(files, errors) {}
147
+ onWatcherRerun(files, trigger) {}
148
+ onServerRestart(reason) {}
149
+ onProcessTimeout() {}
150
+ */
151
+
152
+ /**
153
+ * Recursively gets all tasks from suite and pushes them to "tests" array
154
+ *
155
+ * @param {VitestSuite} suite
156
+ */
157
+ #processTasksOfSuite(suite) {
158
+ getTasks(suite).forEach(taskOrSuite => {
159
+ if (taskOrSuite.type === 'test') {
160
+ const test = taskOrSuite;
161
+ this.tests.push(this.#getDataFromTest(test));
162
+ } else if (taskOrSuite.type === 'suite') {
163
+ const theSuite = taskOrSuite;
164
+ this.#processTasksOfSuite(theSuite);
165
+ } else {
166
+ throw new Error('Unprocessed case. Unknown task type');
167
+ }
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Processes task and returns test data ready to be sent to Testomat.io
173
+ *
174
+ * @param {any} test
175
+ *
176
+ * @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
177
+ */
178
+ #getDataFromTest(test) {
179
+ const normalized = normalizeVitestTest(test);
180
+ const reportKey = getReportKey(test, normalized);
181
+ const startMicros =
182
+ typeof normalized.startTime === 'number'
183
+ ? Math.floor(normalized.startTime * 1000)
184
+ : this._runStartedAtMicros || undefined;
185
+
186
+ return {
187
+ _reportKey: reportKey,
188
+ error: normalized.error,
189
+ file: normalized.file,
190
+ logs: normalized.logs,
191
+ meta: normalized.meta,
192
+ // @ts-ignore - STATUS values are string literals but type system sees them as string
193
+ status: getTestStatus(normalized.state, normalized.mode),
194
+ suite_title: normalized.suiteTitle,
195
+ test_id: getTestomatIdFromTestTitle(normalized.name),
196
+ time: normalized.duration,
197
+ timestamp: startMicros,
198
+ title: normalized.name,
199
+ // testomatio functions (artifacts, logs, steps, meta) are not supported
200
+ };
201
+ }
202
+
203
+ /**
204
+ * @param {unknown} testCase
205
+ */
206
+ async #reportLive(testCase) {
207
+ if (this._finalized || this._finalizing) return;
208
+ const normalized = normalizeVitestTest(testCase);
209
+ if (!isLiveReportableState(normalized.state, normalized.mode)) return;
210
+
211
+ const data = this.#getDataFromTest(testCase);
212
+ if (!data._reportKey || this._reportedTestKeys.has(data._reportKey)) return;
213
+ this._reportedTestKeys.add(data._reportKey);
214
+
215
+ this._liveQueue = this._liveQueue
216
+ .then(() => this.client.addTestRun(data.status, data))
217
+ .catch(() => undefined);
218
+ await this._liveQueue;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Returns run status based on test results
224
+ *
225
+ * @param {VitestTestFile[]} files
226
+ * @returns {'passed' | 'failed' | 'finished'}
227
+ */
228
+ function getRunStatusFromResults(files) {
229
+ /**
230
+ * @type {'passed' | 'failed' | 'finished'}
231
+ */
232
+ let status = 'finished'; // default status (if no failed or passed tests)
233
+
234
+ files.forEach(file => {
235
+ getTasks(file).forEach(taskOrSuite => {
236
+ if (isFailedState(taskOrSuite?.result?.state)) {
237
+ status = 'failed'; // set status to failed if any test failed
238
+ }
239
+ });
240
+
241
+ // if there are no failed tests > search for passed tests
242
+ if (status !== 'failed') {
243
+ getTasks(file).forEach(taskOrSuite => {
244
+ if (isPassedState(taskOrSuite?.result?.state)) {
245
+ status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
246
+ }
247
+ });
248
+ }
249
+ });
250
+
251
+ return status;
252
+ }
253
+
254
+ /**
255
+ * Returns test status in Testomat.io format
256
+ *
257
+ * @param {string | undefined} state
258
+ * @param {string | undefined} mode
259
+ * @returns 'passed' | 'failed' | 'skipped'
260
+ */
261
+ function getTestStatus(state, mode) {
262
+ if (isFailedState(state)) return STATUS.FAILED;
263
+ if (isPassedState(state)) return STATUS.PASSED;
264
+ if (isSkippedState(state) || (!state && mode === 'skip')) return STATUS.SKIPPED;
265
+ console.error(pc.red('Unprocessed case for defining test status. Contact dev team. State:'), state);
266
+ return STATUS.SKIPPED;
267
+ }
268
+
269
+ /**
270
+ * @param {VitestTestLogs[]} logs
271
+ * @returns string
272
+ */
273
+ function transformLogsToString(logs) {
274
+ if (!logs) return '';
275
+ let logsStr = '';
276
+ logs.forEach(log => {
277
+ if (log.type === 'stdout') logsStr += `${log.content}\n`;
278
+ if (log.type === 'stderr') logsStr += `${pc.red(log.content)}\n`;
279
+ });
280
+ return logsStr;
281
+ }
282
+
283
+ /**
284
+ * Supports both old and new Vitest task tree shapes.
285
+ *
286
+ * @param {any} node
287
+ * @returns {any[]}
288
+ */
289
+ function getTasks(node) {
290
+ if (!node) return [];
291
+ if (Array.isArray(node.tasks)) return node.tasks;
292
+ if (Array.isArray(node.children)) return node.children;
293
+ if (node.children && typeof node.children[Symbol.iterator] === 'function') return Array.from(node.children);
294
+ if (node.task) return [node.task];
295
+ return [];
296
+ }
297
+
298
+ /**
299
+ * @param {string | undefined} state
300
+ * @returns {boolean}
301
+ */
302
+ function isFailedState(state) {
303
+ return state === 'fail' || state === 'failed';
304
+ }
305
+
306
+ /**
307
+ * @param {string | undefined} state
308
+ * @returns {boolean}
309
+ */
310
+ function isPassedState(state) {
311
+ return state === 'pass' || state === 'passed';
312
+ }
313
+
314
+ /**
315
+ * @param {string | undefined} state
316
+ * @returns {boolean}
317
+ */
318
+ function isSkippedState(state) {
319
+ return state === 'skip' || state === 'skipped' || state === 'todo';
320
+ }
321
+
322
+ /**
323
+ * Accept only completed test states for live upload to avoid reporting
324
+ * intermediate task updates as skipped.
325
+ *
326
+ * @param {string | undefined} state
327
+ * @param {string | undefined} mode
328
+ * @returns {boolean}
329
+ */
330
+ function isLiveReportableState(state, mode) {
331
+ if (isFailedState(state) || isPassedState(state) || isSkippedState(state)) return true;
332
+ if (!state && mode === 'skip') return true;
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * @param {VitestTestFile[] | undefined} files
338
+ * @returns {number | null}
339
+ */
340
+ function getEarliestTestStartMs(files) {
341
+ let earliest = null;
342
+ const walk = node => {
343
+ if (!node) return;
344
+ const startTime = node?.result?.startTime;
345
+ if (typeof startTime === 'number' && !Number.isNaN(startTime)) {
346
+ if (earliest == null || startTime < earliest) earliest = startTime;
347
+ }
348
+ getTasks(node).forEach(walk);
349
+ };
350
+ (files || []).forEach(walk);
351
+ return earliest;
352
+ }
353
+
354
+ /**
355
+ * @param {any} test
356
+ * @returns {{
357
+ * name: string,
358
+ * state: string | undefined,
359
+ * mode: string | undefined,
360
+ * duration: number,
361
+ * startTime: number | undefined,
362
+ * error: any,
363
+ * file: string,
364
+ * suiteTitle: string,
365
+ * logs: string,
366
+ * meta: any
367
+ * }}
368
+ */
369
+ function normalizeVitestTest(test) {
370
+ if (test && typeof test.result === 'function') {
371
+ const result = test.result();
372
+ const diagnostic = typeof test.diagnostic === 'function' ? test.diagnostic() : undefined;
373
+ const state = result?.state;
374
+ const duration = diagnostic?.duration || 0;
375
+ const startTime = diagnostic?.startTime;
376
+ const error = Array.isArray(result?.errors) ? result.errors[0] : undefined;
377
+ const file =
378
+ test.module?.relativeModuleId ||
379
+ test.module?.moduleId ||
380
+ test.task?.file?.name ||
381
+ test.task?.file?.filepath ||
382
+ '';
383
+ const suiteTitle =
384
+ (test.parent?.type === 'suite' ? test.parent?.name : null) ||
385
+ test.task?.suite?.name ||
386
+ test.task?.file?.name ||
387
+ file;
388
+
389
+ return {
390
+ name: test.name || test.task?.name || '',
391
+ state,
392
+ mode: test.options?.mode || test.task?.mode,
393
+ duration,
394
+ startTime,
395
+ error,
396
+ file,
397
+ suiteTitle,
398
+ logs: '',
399
+ meta: typeof test.meta === 'function' ? test.meta() : {},
400
+ };
401
+ }
402
+
403
+ return {
404
+ name: test?.name || '',
405
+ state: test?.result?.state,
406
+ mode: test?.mode,
407
+ duration: test?.result?.duration || 0,
408
+ startTime: test?.result?.startTime,
409
+ error: test?.result?.errors ? test.result.errors[0] : undefined,
410
+ file: test?.file?.name || test?.file?.filepath || '',
411
+ suiteTitle: test?.suite?.name || test?.file?.name || test?.file?.filepath || '',
412
+ logs: test?.logs ? transformLogsToString(test.logs) : '',
413
+ meta: test?.meta,
414
+ };
415
+ }
416
+
417
+ /**
418
+ * @param {any} test
419
+ * @param {{file: string, suiteTitle: string, name: string, startTime?: number}} normalized
420
+ * @returns {string | null}
421
+ */
422
+ function getReportKey(test, normalized) {
423
+ if (test?.id) return String(test.id);
424
+ if (test?.task?.id) return String(test.task.id);
425
+ if (!normalized?.name) return null;
426
+ const loc = test?.location || test?.task?.location;
427
+ const locationKey = loc ? `${loc.line || ''}:${loc.column || ''}` : '';
428
+ const startKey =
429
+ typeof normalized.startTime === 'number' && !Number.isNaN(normalized.startTime) ? String(normalized.startTime) : '';
430
+ return `${normalized.file}::${normalized.suiteTitle}::${normalized.name}::${locationKey}::${startKey}`;
431
+ }
432
+
433
+ /**
434
+ * Vitest can pass task updates as tuples. Try to extract a test-like object.
435
+ *
436
+ * @param {unknown} pack
437
+ * @returns {any | null}
438
+ */
439
+ function getTestFromTaskUpdatePack(pack) {
440
+ if (!pack) return null;
441
+
442
+ if (Array.isArray(pack)) {
443
+ if (pack[2]?.type === 'test') return pack[2];
444
+ if (pack[1]?.type === 'test') return pack[1];
445
+ if (pack[0]?.type === 'test') return pack[0];
446
+ return null;
447
+ }
448
+
449
+ const objectPack = /** @type {any} */ (pack);
450
+ if (typeof objectPack === 'object' && objectPack?.type === 'test') return objectPack;
451
+ return null;
452
+ }
453
+
454
+ export default VitestReporter;
455
+ export { VitestReporter };
@@ -0,0 +1,201 @@
1
+ import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
2
+ import TestomatClient from '../client.js';
3
+ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
4
+ import { services } from '../services/index.js';
5
+ import { TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
6
+ import { stringToMD5Hash } from '../data-storage.js';
7
+
8
+ class WebdriverReporter extends WDIOReporter {
9
+ constructor(options) {
10
+ super(options);
11
+
12
+ this.client = new TestomatClient({ apiKey: options?.apiKey });
13
+ options = Object.assign(options, { stdout: true });
14
+
15
+ this._addTestPromises = [];
16
+
17
+ this._isSynchronising = false;
18
+
19
+ // Optional hooks enhancer for beforeEach failure handling
20
+ this.hooksEnhancer = null;
21
+ if (options?.enableHooksEnhancer) {
22
+ this._initializeHooksEnhancer();
23
+ }
24
+
25
+ // run is created by cli, if enabling the row below, it mat lead to multiple runs being created
26
+ // thus, need to check if process.env.runId is set and/or add more checks to avoid creating multiple runs
27
+ // this.client.createRun();
28
+ }
29
+
30
+ get isSynchronised() {
31
+ return this._isSynchronising === false;
32
+ }
33
+
34
+ /**
35
+ * Initialize the hooks enhancer
36
+ * @private
37
+ */
38
+ async _initializeHooksEnhancer() {
39
+ try {
40
+ // Dynamic import to avoid hard dependency
41
+ // Resolve package path from the project's node_modules
42
+ const { createRequire } = await import('module');
43
+ const projectRequire = createRequire(process.cwd() + '/package.json');
44
+ // Import the hooks enhancer package
45
+ const packagePath = projectRequire.resolve('@testomatio/webdriver-hooks-enhancer');
46
+ const hooksEnhancerModule = await import(packagePath);
47
+ const { createHooksEnhancer } = hooksEnhancerModule;
48
+
49
+ this.hooksEnhancer = createHooksEnhancer(this);
50
+ console.log('[TESTOMATIO] WebdriverIO Hooks Enhancer enabled');
51
+ } catch (error) {
52
+ console.warn(
53
+ '[TESTOMATIO] Could not enable WebdriverIO Hooks Enhancer.',
54
+ 'Install @testomatio/webdriver-hooks-enhancer to use this feature:',
55
+ error.message
56
+ );
57
+ }
58
+ }
59
+
60
+ /**
61
+ *
62
+ * @param {RunnerStats} runData
63
+ */
64
+ async onRunnerEnd(runData) {
65
+ this._isSynchronising = true;
66
+
67
+ await Promise.all(this._addTestPromises);
68
+
69
+ this._isSynchronising = false;
70
+
71
+ // NOTE: new functionality; may break everything
72
+ // also this may require additional status mapping
73
+ await this.client.updateRunStatus(runData.failures ? 'failed' : 'passed');
74
+ }
75
+
76
+ onRunnerStart() {
77
+ // clear dir with artifacts/logs
78
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
79
+ }
80
+
81
+ onHookEnd(hook) {
82
+ // Hooks enhancer will handle this if enabled
83
+ if (this.hooksEnhancer) {
84
+ this.hooksEnhancer.trackHookFailure(hook);
85
+ }
86
+ }
87
+
88
+ async onSuiteEnd(suiteOrScenario) {
89
+ // Handle hook failures for regular suites using enhancer
90
+ if (this.hooksEnhancer && suiteOrScenario.type !== 'scenario') {
91
+ await this.hooksEnhancer.handleSuiteEnd(
92
+ suiteOrScenario,
93
+ this.client,
94
+ getTestomatIdFromTestTitle
95
+ );
96
+ }
97
+
98
+ // Handle BDD scenarios (cucumber)
99
+ if (suiteOrScenario.type === 'scenario') {
100
+ this._addTestPromises.push(this.addBddScenario(suiteOrScenario));
101
+ }
102
+ }
103
+
104
+ onTestStart(test) {
105
+ services.setContext(test.fullTitle);
106
+ }
107
+
108
+ onTestEnd(test) {
109
+ test.suite = test.parent;
110
+ const logs = getTestLogs(test.fullTitle);
111
+
112
+ test.artifacts = services.artifacts.get(test.fullTitle);
113
+ test.meta = services.keyValues.get(test.fullTitle);
114
+ test.links = services.links.get(test.fullTitle);
115
+ test.logs = logs;
116
+
117
+ this._addTestPromises.push(this.addTest(test));
118
+ }
119
+
120
+ async addTest(test) {
121
+ if (!this.client) return;
122
+
123
+ const { title, _duration: duration, state, error, output, links, artifacts, meta, logs } = test;
124
+
125
+ const testId = getTestomatIdFromTestTitle(title);
126
+
127
+ const screenshotEndpoint = '/session/:sessionId/screenshot';
128
+ const screenshotsBuffers = output
129
+ .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
130
+ .map(el => Buffer.from(el.result.value, 'base64'));
131
+
132
+ const rid = stringToMD5Hash(test.fullTitle);
133
+
134
+ await this.client.addTestRun(state, {
135
+ rid,
136
+ manuallyAttachedArtifacts: test.artifacts,
137
+ error,
138
+ logs,
139
+ meta,
140
+ links,
141
+ title,
142
+ test_id: testId,
143
+ time: duration,
144
+ filesBuffers: screenshotsBuffers,
145
+ });
146
+ }
147
+
148
+ /**
149
+ * @param {import('../../types/types.js').WebdriverIOScenario} scenario
150
+ */
151
+ addBddScenario(scenario) {
152
+ if (!this.client) return;
153
+
154
+ const { title, _duration: duration } = scenario;
155
+
156
+ const testId = getTestomatIdFromTestTitle(title || scenario.tags.map(tag => tag.name).join(' '));
157
+
158
+ let scenarioState = scenario.tests.every(test => test.state === 'passed') ? 'passed' : 'failed';
159
+ if (scenario.tests.every(test => test.state === 'skipped')) {
160
+ scenarioState = 'skipped';
161
+ }
162
+ const errors = scenario.tests
163
+ .filter(test => test.state === 'failed')
164
+ .map(test => test.error?.stack)
165
+ .filter(Boolean);
166
+ const error = errors.join('\n');
167
+
168
+ const tags = scenario.tags.map(tag => tag.name);
169
+
170
+ return this.client.addTestRun(scenarioState, {
171
+ error: error ? Error(error) : null,
172
+ title,
173
+ test_id: testId,
174
+ time: duration,
175
+ tags,
176
+ file: scenario.file,
177
+ // filesBuffers: screenshotsBuffers,
178
+ });
179
+ }
180
+ }
181
+
182
+ /**
183
+ *
184
+ * @param {*} fullTestTitle
185
+ * @returns string
186
+ */
187
+ function getTestLogs(fullTestTitle) {
188
+ const logsArr = services.logger.getLogs(fullTestTitle);
189
+ // remove duplicates (for some reason, logs are duplicated several times)
190
+ const logs = logsArr ? Array.from(new Set(logsArr)).join('\n').trim() : '';
191
+ return logs;
192
+ }
193
+
194
+ export default WebdriverReporter;
195
+
196
+ /* INVESTIGATION RESULTS:
197
+ If you run tests in parallel, the WDIO creates a separate process for each parallel instance.
198
+ As a result, there is own WDIOReporter instance for each parallel process.
199
+ This means, its impossible to create or finish run, because can't understand if its was already created
200
+ in other process or not.
201
+ */