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.
- package/README.md +3 -3
- package/bin/cli.js +6 -26
- package/package.json +39 -4
- package/src/adapter/codecept.js +626 -0
- package/src/adapter/cucumber/current.js +230 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +108 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +211 -0
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +343 -0
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +455 -0
- package/src/adapter/webdriver.js +201 -0
- package/src/bin/cli.js +507 -0
- package/src/bin/reportXml.js +79 -0
- package/src/bin/startTest.js +54 -0
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +524 -0
- package/src/config.js +30 -0
- package/src/constants.js +72 -0
- package/src/data-storage.js +204 -0
- package/src/helpers.js +1 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +70 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +285 -0
- package/src/pipe/coverage.js +500 -0
- package/src/pipe/csv.js +161 -0
- package/src/pipe/debug.js +143 -0
- package/src/pipe/github.js +256 -0
- package/src/pipe/gitlab.js +258 -0
- package/src/pipe/html.js +1153 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/markdown.js +753 -0
- package/src/pipe/testomatio.js +707 -0
- package/src/replay.js +274 -0
- package/src/reporter-functions.js +155 -0
- package/src/reporter.js +42 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +15 -0
- package/src/services/key-values.js +59 -0
- package/src/services/links.js +69 -0
- package/src/services/logger.js +320 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3726 -0
- package/src/uploader.js +382 -0
- package/src/utils/constants.js +12 -0
- package/src/utils/debug.js +20 -0
- package/src/utils/log-formatter.js +118 -0
- package/src/utils/log.js +88 -0
- package/src/utils/pipe_utils.js +193 -0
- package/src/utils/utils.js +732 -0
- package/src/xmlReader.js +834 -0
- package/types/types.d.ts +425 -0
- 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
|
+
*/
|