@testomatio/reporter 2.7.4-beta.allure-1 → 2.7.4
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 +9 -4
- package/lib/adapter/vitest.d.ts +22 -3
- package/lib/adapter/vitest.js +250 -26
- package/lib/bin/cli.js +0 -28
- package/lib/client.d.ts +2 -1
- package/lib/client.js +3 -2
- package/lib/junit-adapter/index.js +0 -4
- package/lib/pipe/bitbucket.js +16 -1
- package/lib/pipe/html.js +24 -5
- package/lib/template/testomatio.hbs +131 -20
- package/lib/utils/utils.js +0 -9
- package/lib/xmlReader.js +7 -5
- package/package.json +1 -1
- package/src/adapter/vitest.js +253 -26
- package/src/bin/cli.js +0 -35
- package/src/client.js +3 -2
- package/src/junit-adapter/index.js +0 -4
- package/src/pipe/bitbucket.js +23 -1
- package/src/pipe/html.js +24 -5
- package/src/template/testomatio.hbs +131 -20
- package/src/utils/utils.js +0 -5
- package/src/xmlReader.js +9 -6
- package/lib/allureReader.d.ts +0 -65
- package/lib/allureReader.js +0 -461
- package/lib/junit-adapter/kotlin.d.ts +0 -5
- package/lib/junit-adapter/kotlin.js +0 -46
- package/src/allureReader.js +0 -540
- package/src/junit-adapter/kotlin.js +0 -48
package/README.md
CHANGED
|
@@ -58,6 +58,13 @@ pnpm install @testomatio/reporter --save-dev
|
|
|
58
58
|
yarn add @testomatio/reporter --dev
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
For Yarn 4 (Berry), use CLI wrapper package:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
yarn add testomatio-reporter-cli --dev
|
|
65
|
+
npx testomatio-reporter <command> [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
61
68
|
## Getting Started
|
|
62
69
|
|
|
63
70
|
### 1️⃣ Attach Reporter to the Test Runner
|
|
@@ -69,7 +76,6 @@ yarn add @testomatio/reporter --dev
|
|
|
69
76
|
| [TestCafe](./docs/frameworks.md#testcafe) | [Detox](./docs/frameworks.md#detox) | [Codeception](https://github.com/testomatio/php-reporter) |
|
|
70
77
|
| [Newman (Postman)](./docs/frameworks.md#newman) | [JUnit](./docs/junit.md#junit) | [NUnit](./docs/junit.md#nunit) |
|
|
71
78
|
| [PyTest](./docs/junit.md#pytest) | [PHPUnit](./docs/junit.md#phpunit) | [Protractor](./docs/frameworks.md#protractor) |
|
|
72
|
-
| [Allure](./docs/allure.md) | | |
|
|
73
79
|
|
|
74
80
|
or **any [other via JUnit](./docs/junit.md)** report....
|
|
75
81
|
|
|
@@ -130,10 +136,9 @@ Bring this reporter on CI and never lose test results again!
|
|
|
130
136
|
- [CSV](./docs/pipes/csv.md)
|
|
131
137
|
- [HTML report](./docs/pipes/html.md)
|
|
132
138
|
- [Bitbucket](./docs/pipes/bitbucket.md)
|
|
133
|
-
- 📓 [JUnit Reports](./docs/junit.md)
|
|
134
|
-
- 🗄️ [Artifacts](./docs/artifacts.md)
|
|
135
|
-
- 🔬 [Allure Reports](./docs/allure.md)
|
|
136
139
|
- 🔗 [Linking Tests](./docs/linking-tests.md)
|
|
140
|
+
- 📓 [JUnit](./docs/junit.md)
|
|
141
|
+
- 🗄️ [Artifacts](./docs/artifacts.md)
|
|
137
142
|
- 🔂 [Workflows](./docs/workflows.md)
|
|
138
143
|
- 🖊️ [Logger](./docs/logger.md)
|
|
139
144
|
- 🪲 [Debug File Format](./docs/debug-file-format.md)
|
package/lib/adapter/vitest.d.ts
CHANGED
|
@@ -18,15 +18,22 @@ export type TestData = import("../../types/types.js").TestData;
|
|
|
18
18
|
export class VitestReporter {
|
|
19
19
|
constructor(config?: {});
|
|
20
20
|
client: TestomatioClient;
|
|
21
|
-
/**
|
|
22
|
-
* @type {(TestData & {status: string})[]} tests
|
|
23
|
-
*/
|
|
21
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
24
22
|
tests: (TestData & {
|
|
25
23
|
status: string;
|
|
24
|
+
_reportKey?: string | null;
|
|
26
25
|
})[];
|
|
27
26
|
_finalized: boolean;
|
|
28
27
|
_finalizing: boolean;
|
|
28
|
+
_runStartedAtMs: number;
|
|
29
|
+
_runStartedAtMicros: number;
|
|
30
|
+
_reportedTestKeys: Set<any>;
|
|
31
|
+
_liveQueue: Promise<void>;
|
|
29
32
|
onInit(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
35
|
+
*/
|
|
36
|
+
onTestRunStart(): void;
|
|
30
37
|
/**
|
|
31
38
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
32
39
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -39,6 +46,18 @@ export class VitestReporter {
|
|
|
39
46
|
* @param {unknown[] | undefined} errors
|
|
40
47
|
*/
|
|
41
48
|
onTestRunEnd(testModules: Array<unknown> | undefined, errors: unknown[] | undefined): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Vitest 4 callback fired when single test case is finished.
|
|
51
|
+
*
|
|
52
|
+
* @param {unknown} testCase
|
|
53
|
+
*/
|
|
54
|
+
onTestCaseResult(testCase: unknown): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Vitest 3 fallback callback with task updates.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown[] | undefined} packs
|
|
59
|
+
*/
|
|
60
|
+
onTaskUpdate(packs: unknown[] | undefined): Promise<void>;
|
|
42
61
|
#private;
|
|
43
62
|
}
|
|
44
63
|
import { Client as TestomatioClient } from '../client.js';
|
package/lib/adapter/vitest.js
CHANGED
|
@@ -22,19 +22,34 @@ const debug = (0, debug_1.default)('@testomatio/reporter:adapter-jest');
|
|
|
22
22
|
class VitestReporter {
|
|
23
23
|
constructor(config = {}) {
|
|
24
24
|
this.client = new client_js_1.Client({ apiKey: config?.apiKey });
|
|
25
|
-
/**
|
|
26
|
-
* @type {(TestData & {status: string})[]} tests
|
|
27
|
-
*/
|
|
25
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
28
26
|
this.tests = [];
|
|
29
27
|
this._finalized = false;
|
|
30
28
|
this._finalizing = false;
|
|
29
|
+
this._runStartedAtMs = null;
|
|
30
|
+
this._runStartedAtMicros = null;
|
|
31
|
+
this._reportedTestKeys = new Set();
|
|
32
|
+
this._liveQueue = Promise.resolve();
|
|
31
33
|
}
|
|
32
34
|
// on run start
|
|
33
35
|
onInit() {
|
|
36
|
+
const now = Date.now();
|
|
34
37
|
this._finalized = false;
|
|
35
38
|
this._finalizing = false;
|
|
39
|
+
this._runStartedAtMs = now;
|
|
40
|
+
this._runStartedAtMicros = now * 1000;
|
|
41
|
+
this._reportedTestKeys = new Set();
|
|
42
|
+
this._liveQueue = Promise.resolve();
|
|
36
43
|
this.client.createRun();
|
|
37
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
47
|
+
*/
|
|
48
|
+
onTestRunStart() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
this._runStartedAtMs = now;
|
|
51
|
+
this._runStartedAtMicros = now * 1000;
|
|
52
|
+
}
|
|
38
53
|
/**
|
|
39
54
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
40
55
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -68,12 +83,19 @@ class VitestReporter {
|
|
|
68
83
|
debug(this.tests.length, 'tests collected');
|
|
69
84
|
// send tests to Testomat.io
|
|
70
85
|
for (const test of this.tests) {
|
|
86
|
+
if (test._reportKey && this._reportedTestKeys.has(test._reportKey))
|
|
87
|
+
continue;
|
|
88
|
+
if (test._reportKey)
|
|
89
|
+
this._reportedTestKeys.add(test._reportKey);
|
|
71
90
|
await this.client.addTestRun(test.status, test);
|
|
72
91
|
}
|
|
92
|
+
await this._liveQueue;
|
|
73
93
|
console.log('finished');
|
|
74
94
|
if (errors.length)
|
|
75
95
|
console.error('Vitest adapter errors:', errors);
|
|
76
|
-
|
|
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 });
|
|
77
99
|
this._finalized = true;
|
|
78
100
|
}
|
|
79
101
|
finally {
|
|
@@ -92,6 +114,28 @@ class VitestReporter {
|
|
|
92
114
|
.filter(Boolean);
|
|
93
115
|
await this.onFinished(files, errors);
|
|
94
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Vitest 4 callback fired when single test case is finished.
|
|
119
|
+
*
|
|
120
|
+
* @param {unknown} testCase
|
|
121
|
+
*/
|
|
122
|
+
async onTestCaseResult(testCase) {
|
|
123
|
+
await this.#reportLive(testCase);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Vitest 3 fallback callback with task updates.
|
|
127
|
+
*
|
|
128
|
+
* @param {unknown[] | undefined} packs
|
|
129
|
+
*/
|
|
130
|
+
async onTaskUpdate(packs) {
|
|
131
|
+
if (!Array.isArray(packs) || !packs.length)
|
|
132
|
+
return;
|
|
133
|
+
for (const pack of packs) {
|
|
134
|
+
const test = getTestFromTaskUpdatePack(pack);
|
|
135
|
+
if (test)
|
|
136
|
+
await this.#reportLive(test);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
95
139
|
/* non-used listeners
|
|
96
140
|
onUserConsoleLog(log) {}
|
|
97
141
|
onPathsCollected(paths) {} // paths array to files with tests
|
|
@@ -126,25 +170,50 @@ class VitestReporter {
|
|
|
126
170
|
/**
|
|
127
171
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
128
172
|
*
|
|
129
|
-
* @param {
|
|
173
|
+
* @param {any} test
|
|
130
174
|
*
|
|
131
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
175
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
|
|
132
176
|
*/
|
|
133
177
|
#getDataFromTest(test) {
|
|
178
|
+
const normalized = normalizeVitestTest(test);
|
|
179
|
+
const reportKey = getReportKey(test, normalized);
|
|
180
|
+
const startMicros = typeof normalized.startTime === 'number'
|
|
181
|
+
? Math.floor(normalized.startTime * 1000)
|
|
182
|
+
: this._runStartedAtMicros || undefined;
|
|
134
183
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
184
|
+
_reportKey: reportKey,
|
|
185
|
+
error: normalized.error,
|
|
186
|
+
file: normalized.file,
|
|
187
|
+
logs: normalized.logs,
|
|
188
|
+
meta: normalized.meta,
|
|
139
189
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
140
|
-
status: getTestStatus(
|
|
141
|
-
suite_title:
|
|
142
|
-
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(
|
|
143
|
-
time:
|
|
144
|
-
|
|
190
|
+
status: getTestStatus(normalized.state, normalized.mode),
|
|
191
|
+
suite_title: normalized.suiteTitle,
|
|
192
|
+
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(normalized.name),
|
|
193
|
+
time: normalized.duration,
|
|
194
|
+
timestamp: startMicros,
|
|
195
|
+
title: normalized.name,
|
|
145
196
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
146
197
|
};
|
|
147
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown} testCase
|
|
201
|
+
*/
|
|
202
|
+
async #reportLive(testCase) {
|
|
203
|
+
if (this._finalized || this._finalizing)
|
|
204
|
+
return;
|
|
205
|
+
const normalized = normalizeVitestTest(testCase);
|
|
206
|
+
if (!isLiveReportableState(normalized.state, normalized.mode))
|
|
207
|
+
return;
|
|
208
|
+
const data = this.#getDataFromTest(testCase);
|
|
209
|
+
if (!data._reportKey || this._reportedTestKeys.has(data._reportKey))
|
|
210
|
+
return;
|
|
211
|
+
this._reportedTestKeys.add(data._reportKey);
|
|
212
|
+
this._liveQueue = this._liveQueue
|
|
213
|
+
.then(() => this.client.addTestRun(data.status, data))
|
|
214
|
+
.catch(() => undefined);
|
|
215
|
+
await this._liveQueue;
|
|
216
|
+
}
|
|
148
217
|
}
|
|
149
218
|
exports.VitestReporter = VitestReporter;
|
|
150
219
|
/**
|
|
@@ -159,16 +228,15 @@ function getRunStatusFromResults(files) {
|
|
|
159
228
|
*/
|
|
160
229
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
161
230
|
files.forEach(file => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (taskOrSuite.result?.state === 'fail') {
|
|
231
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
232
|
+
if (isFailedState(taskOrSuite?.result?.state)) {
|
|
165
233
|
status = 'failed'; // set status to failed if any test failed
|
|
166
234
|
}
|
|
167
235
|
});
|
|
168
236
|
// if there are no failed tests > search for passed tests
|
|
169
237
|
if (status !== 'failed') {
|
|
170
|
-
file.
|
|
171
|
-
if (taskOrSuite
|
|
238
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
239
|
+
if (isPassedState(taskOrSuite?.result?.state)) {
|
|
172
240
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
173
241
|
}
|
|
174
242
|
});
|
|
@@ -179,17 +247,18 @@ function getRunStatusFromResults(files) {
|
|
|
179
247
|
/**
|
|
180
248
|
* Returns test status in Testomat.io format
|
|
181
249
|
*
|
|
182
|
-
* @param {
|
|
250
|
+
* @param {string | undefined} state
|
|
251
|
+
* @param {string | undefined} mode
|
|
183
252
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
184
253
|
*/
|
|
185
|
-
function getTestStatus(
|
|
186
|
-
if (
|
|
254
|
+
function getTestStatus(state, mode) {
|
|
255
|
+
if (isFailedState(state))
|
|
187
256
|
return constants_js_1.STATUS.FAILED;
|
|
188
|
-
if (
|
|
257
|
+
if (isPassedState(state))
|
|
189
258
|
return constants_js_1.STATUS.PASSED;
|
|
190
|
-
if (
|
|
259
|
+
if (isSkippedState(state) || (!state && mode === 'skip'))
|
|
191
260
|
return constants_js_1.STATUS.SKIPPED;
|
|
192
|
-
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team.
|
|
261
|
+
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team. State:'), state);
|
|
193
262
|
return constants_js_1.STATUS.SKIPPED;
|
|
194
263
|
}
|
|
195
264
|
/**
|
|
@@ -221,10 +290,165 @@ function getTasks(node) {
|
|
|
221
290
|
return node.tasks;
|
|
222
291
|
if (Array.isArray(node.children))
|
|
223
292
|
return node.children;
|
|
293
|
+
if (node.children && typeof node.children[Symbol.iterator] === 'function')
|
|
294
|
+
return Array.from(node.children);
|
|
224
295
|
if (node.task)
|
|
225
296
|
return [node.task];
|
|
226
297
|
return [];
|
|
227
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* @param {string | undefined} state
|
|
301
|
+
* @returns {boolean}
|
|
302
|
+
*/
|
|
303
|
+
function isFailedState(state) {
|
|
304
|
+
return state === 'fail' || state === 'failed';
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @param {string | undefined} state
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
function isPassedState(state) {
|
|
311
|
+
return state === 'pass' || state === 'passed';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* @param {string | undefined} state
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
function isSkippedState(state) {
|
|
318
|
+
return state === 'skip' || state === 'skipped' || state === 'todo';
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Accept only completed test states for live upload to avoid reporting
|
|
322
|
+
* intermediate task updates as skipped.
|
|
323
|
+
*
|
|
324
|
+
* @param {string | undefined} state
|
|
325
|
+
* @param {string | undefined} mode
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
function isLiveReportableState(state, mode) {
|
|
329
|
+
if (isFailedState(state) || isPassedState(state) || isSkippedState(state))
|
|
330
|
+
return true;
|
|
331
|
+
if (!state && mode === 'skip')
|
|
332
|
+
return true;
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* @param {VitestTestFile[] | undefined} files
|
|
337
|
+
* @returns {number | null}
|
|
338
|
+
*/
|
|
339
|
+
function getEarliestTestStartMs(files) {
|
|
340
|
+
let earliest = null;
|
|
341
|
+
const walk = node => {
|
|
342
|
+
if (!node)
|
|
343
|
+
return;
|
|
344
|
+
const startTime = node?.result?.startTime;
|
|
345
|
+
if (typeof startTime === 'number' && !Number.isNaN(startTime)) {
|
|
346
|
+
if (earliest == null || startTime < earliest)
|
|
347
|
+
earliest = startTime;
|
|
348
|
+
}
|
|
349
|
+
getTasks(node).forEach(walk);
|
|
350
|
+
};
|
|
351
|
+
(files || []).forEach(walk);
|
|
352
|
+
return earliest;
|
|
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 = test.module?.relativeModuleId ||
|
|
378
|
+
test.module?.moduleId ||
|
|
379
|
+
test.task?.file?.name ||
|
|
380
|
+
test.task?.file?.filepath ||
|
|
381
|
+
'';
|
|
382
|
+
const suiteTitle = (test.parent?.type === 'suite' ? test.parent?.name : null) ||
|
|
383
|
+
test.task?.suite?.name ||
|
|
384
|
+
test.task?.file?.name ||
|
|
385
|
+
file;
|
|
386
|
+
return {
|
|
387
|
+
name: test.name || test.task?.name || '',
|
|
388
|
+
state,
|
|
389
|
+
mode: test.options?.mode || test.task?.mode,
|
|
390
|
+
duration,
|
|
391
|
+
startTime,
|
|
392
|
+
error,
|
|
393
|
+
file,
|
|
394
|
+
suiteTitle,
|
|
395
|
+
logs: '',
|
|
396
|
+
meta: typeof test.meta === 'function' ? test.meta() : {},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
name: test?.name || '',
|
|
401
|
+
state: test?.result?.state,
|
|
402
|
+
mode: test?.mode,
|
|
403
|
+
duration: test?.result?.duration || 0,
|
|
404
|
+
startTime: test?.result?.startTime,
|
|
405
|
+
error: test?.result?.errors ? test.result.errors[0] : undefined,
|
|
406
|
+
file: test?.file?.name || test?.file?.filepath || '',
|
|
407
|
+
suiteTitle: test?.suite?.name || test?.file?.name || test?.file?.filepath || '',
|
|
408
|
+
logs: test?.logs ? transformLogsToString(test.logs) : '',
|
|
409
|
+
meta: test?.meta,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* @param {any} test
|
|
414
|
+
* @param {{file: string, suiteTitle: string, name: string, startTime?: number}} normalized
|
|
415
|
+
* @returns {string | null}
|
|
416
|
+
*/
|
|
417
|
+
function getReportKey(test, normalized) {
|
|
418
|
+
if (test?.id)
|
|
419
|
+
return String(test.id);
|
|
420
|
+
if (test?.task?.id)
|
|
421
|
+
return String(test.task.id);
|
|
422
|
+
if (!normalized?.name)
|
|
423
|
+
return null;
|
|
424
|
+
const loc = test?.location || test?.task?.location;
|
|
425
|
+
const locationKey = loc ? `${loc.line || ''}:${loc.column || ''}` : '';
|
|
426
|
+
const startKey = typeof normalized.startTime === 'number' && !Number.isNaN(normalized.startTime) ? String(normalized.startTime) : '';
|
|
427
|
+
return `${normalized.file}::${normalized.suiteTitle}::${normalized.name}::${locationKey}::${startKey}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Vitest can pass task updates as tuples. Try to extract a test-like object.
|
|
431
|
+
*
|
|
432
|
+
* @param {unknown} pack
|
|
433
|
+
* @returns {any | null}
|
|
434
|
+
*/
|
|
435
|
+
function getTestFromTaskUpdatePack(pack) {
|
|
436
|
+
if (!pack)
|
|
437
|
+
return null;
|
|
438
|
+
if (Array.isArray(pack)) {
|
|
439
|
+
if (pack[2]?.type === 'test')
|
|
440
|
+
return pack[2];
|
|
441
|
+
if (pack[1]?.type === 'test')
|
|
442
|
+
return pack[1];
|
|
443
|
+
if (pack[0]?.type === 'test')
|
|
444
|
+
return pack[0];
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const objectPack = /** @type {any} */ (pack);
|
|
448
|
+
if (typeof objectPack === 'object' && objectPack?.type === 'test')
|
|
449
|
+
return objectPack;
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
228
452
|
module.exports = VitestReporter;
|
|
229
453
|
|
|
230
454
|
module.exports.VitestReporter = VitestReporter;
|
package/lib/bin/cli.js
CHANGED
|
@@ -10,7 +10,6 @@ const glob_1 = require("glob");
|
|
|
10
10
|
const debug_1 = __importDefault(require("debug"));
|
|
11
11
|
const client_js_1 = __importDefault(require("../client.js"));
|
|
12
12
|
const xmlReader_js_1 = __importDefault(require("../xmlReader.js"));
|
|
13
|
-
const allureReader_js_1 = __importDefault(require("../allureReader.js"));
|
|
14
13
|
const constants_js_1 = require("../constants.js");
|
|
15
14
|
const utils_js_1 = require("../utils/utils.js");
|
|
16
15
|
const config_js_1 = require("../config.js");
|
|
@@ -238,33 +237,6 @@ program
|
|
|
238
237
|
if (timeoutTimer)
|
|
239
238
|
clearTimeout(timeoutTimer);
|
|
240
239
|
});
|
|
241
|
-
program
|
|
242
|
-
.command('allure')
|
|
243
|
-
.description('Parse Allure result files and upload to Testomat.io')
|
|
244
|
-
.argument('<pattern>', 'Allure result directory pattern')
|
|
245
|
-
.option('-d, --dir <dir>', 'Project directory')
|
|
246
|
-
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
247
|
-
.option('--with-package', 'Keep full package path in file names (default: strip package prefix)')
|
|
248
|
-
.action(async (pattern, opts) => {
|
|
249
|
-
const runReader = new allureReader_js_1.default({ withPackage: opts.withPackage });
|
|
250
|
-
let timeoutTimer;
|
|
251
|
-
if (opts.timelimit) {
|
|
252
|
-
timeoutTimer = setTimeout(() => {
|
|
253
|
-
console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
|
|
254
|
-
process.exit(0);
|
|
255
|
-
}, parseInt(opts.timelimit, 10) * 1000);
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
await runReader.parse(pattern);
|
|
259
|
-
await runReader.createRun();
|
|
260
|
-
await runReader.uploadData();
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
console.log(constants_js_1.APP_PREFIX, 'Error uploading Allure results:', err);
|
|
264
|
-
}
|
|
265
|
-
if (timeoutTimer)
|
|
266
|
-
clearTimeout(timeoutTimer);
|
|
267
|
-
});
|
|
268
240
|
program
|
|
269
241
|
.command('upload-artifacts')
|
|
270
242
|
.description('Upload artifacts to Testomat.io')
|
package/lib/client.d.ts
CHANGED
|
@@ -65,9 +65,10 @@ export class Client {
|
|
|
65
65
|
*
|
|
66
66
|
* Updates the status of the current test run and finishes the run.
|
|
67
67
|
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
68
|
+
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
68
69
|
* Must be one of "passed", "failed", or "finished"
|
|
69
70
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
70
71
|
*/
|
|
71
|
-
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
|
|
72
|
+
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
72
73
|
}
|
|
73
74
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -308,17 +308,18 @@ class Client {
|
|
|
308
308
|
*
|
|
309
309
|
* Updates the status of the current test run and finishes the run.
|
|
310
310
|
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
311
|
+
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
311
312
|
* Must be one of "passed", "failed", or "finished"
|
|
312
313
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
313
314
|
*/
|
|
314
|
-
async updateRunStatus(status) {
|
|
315
|
+
async updateRunStatus(status, params = {}) {
|
|
315
316
|
this.pipes ||= await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
316
317
|
this.runId ||= (0, utils_js_1.readLatestRunId)();
|
|
317
318
|
debug('Updating run status...');
|
|
318
319
|
// all pipes disabled, skipping
|
|
319
320
|
if (!this.pipes?.filter(p => p.isEnabled).length)
|
|
320
321
|
return Promise.resolve();
|
|
321
|
-
const runParams = { status };
|
|
322
|
+
const runParams = { ...params, status };
|
|
322
323
|
this.queue = this.queue
|
|
323
324
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
324
325
|
.then(() => {
|
|
@@ -9,7 +9,6 @@ const java_js_1 = __importDefault(require("./java.js"));
|
|
|
9
9
|
const python_js_1 = __importDefault(require("./python.js"));
|
|
10
10
|
const ruby_js_1 = __importDefault(require("./ruby.js"));
|
|
11
11
|
const csharp_js_1 = __importDefault(require("./csharp.js"));
|
|
12
|
-
const kotlin_js_1 = __importDefault(require("./kotlin.js"));
|
|
13
12
|
function AdapterFactory(lang, opts) {
|
|
14
13
|
if (lang === 'java') {
|
|
15
14
|
return new java_js_1.default(opts);
|
|
@@ -26,9 +25,6 @@ function AdapterFactory(lang, opts) {
|
|
|
26
25
|
if (lang === 'c#' || lang === 'csharp') {
|
|
27
26
|
return new csharp_js_1.default(opts);
|
|
28
27
|
}
|
|
29
|
-
if (lang === 'kotlin') {
|
|
30
|
-
return new kotlin_js_1.default(opts);
|
|
31
|
-
}
|
|
32
28
|
return new adapter_js_1.default(opts);
|
|
33
29
|
}
|
|
34
30
|
module.exports = AdapterFactory;
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -101,6 +101,14 @@ class BitbucketPipe {
|
|
|
101
101
|
async finishRun(runParams) {
|
|
102
102
|
if (!this.isEnabled)
|
|
103
103
|
return;
|
|
104
|
+
if (!this.ENV.BITBUCKET_PR_ID) {
|
|
105
|
+
log_js_1.log.warn(picocolors_1.default.yellow('Bitbucket'), 'Skipping PR comment: BITBUCKET_PR_ID is not set. Run this pipe in a Bitbucket pull-requests pipeline.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!this.ENV.BITBUCKET_WORKSPACE || !this.ENV.BITBUCKET_REPO_SLUG) {
|
|
109
|
+
log_js_1.log.warn(picocolors_1.default.yellow('Bitbucket'), 'Skipping PR comment: BITBUCKET_WORKSPACE or BITBUCKET_REPO_SLUG is missing.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
104
112
|
if (runParams.tests)
|
|
105
113
|
runParams.tests.forEach(t => this.addTest(t));
|
|
106
114
|
// Clean up the logs from ANSI codes
|
|
@@ -194,9 +202,16 @@ class BitbucketPipe {
|
|
|
194
202
|
log_js_1.log.info(picocolors_1.default.yellow('Bitbucket'), `Report created: ${picocolors_1.default.magenta(commentURL)}`);
|
|
195
203
|
}
|
|
196
204
|
catch (err) {
|
|
205
|
+
const isForbiddenError = `${err}`.includes('Forbidden') || `${err}`.includes('403');
|
|
206
|
+
const scopeHint = isForbiddenError
|
|
207
|
+
? '\nHint: use a token that can write PR comments '
|
|
208
|
+
+ '(recommended: Repository Access Token with Pull requests: Write '
|
|
209
|
+
+ 'and Repository: Read) and run inside a pull-requests pipeline '
|
|
210
|
+
+ 'where BITBUCKET_PR_ID is available.'
|
|
211
|
+
: '';
|
|
197
212
|
console.error(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('Bitbucket'), `Couldn't create Bitbucket report\n${err}.
|
|
198
213
|
Request URL: ${commentsRequestURL}
|
|
199
|
-
Request data: ${body}`);
|
|
214
|
+
Request data: ${body}${scopeHint}`);
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
async sync() {
|
package/lib/pipe/html.js
CHANGED
|
@@ -180,8 +180,12 @@ class HtmlPipe {
|
|
|
180
180
|
...(test.meta?.attachments || []),
|
|
181
181
|
];
|
|
182
182
|
test.artifactsUploaded = allPossibleArtifacts.some(artifact => {
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
let link = artifact?.link || artifact?.path;
|
|
184
|
+
if (!link)
|
|
185
|
+
return false;
|
|
186
|
+
if (typeof link !== 'string')
|
|
187
|
+
link = String(link);
|
|
188
|
+
return link.startsWith('http://') || link.startsWith('https://');
|
|
185
189
|
});
|
|
186
190
|
normalizeRetries(test);
|
|
187
191
|
if (test.traces) {
|
|
@@ -932,6 +936,16 @@ function normalizeArtifacts(test) {
|
|
|
932
936
|
return allArtifacts
|
|
933
937
|
.map(artifact => {
|
|
934
938
|
if (typeof artifact === 'string') {
|
|
939
|
+
if (/^https?:\/\//i.test(artifact)) {
|
|
940
|
+
const base = path_1.default.basename(new URL(artifact).pathname) || artifact;
|
|
941
|
+
return {
|
|
942
|
+
name: base,
|
|
943
|
+
title: base,
|
|
944
|
+
path: artifact,
|
|
945
|
+
fsPath: null,
|
|
946
|
+
relativePath: artifact,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
935
949
|
const abs = path_1.default.isAbsolute(artifact) ? artifact : path_1.default.resolve(process.cwd(), artifact);
|
|
936
950
|
const href = artifact.startsWith('file://') ? artifact : (0, file_url_1.default)(abs, { resolve: true });
|
|
937
951
|
const base = path_1.default.basename(abs);
|
|
@@ -946,9 +960,14 @@ function normalizeArtifacts(test) {
|
|
|
946
960
|
if (artifact?.path) {
|
|
947
961
|
const raw = String(artifact.path);
|
|
948
962
|
const isFileUrl = raw.startsWith('file://');
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
const
|
|
963
|
+
const isHttpUrl = /^https?:\/\//i.test(raw);
|
|
964
|
+
const abs = isFileUrl || isHttpUrl ? null : path_1.default.isAbsolute(raw) ? raw : path_1.default.resolve(process.cwd(), raw);
|
|
965
|
+
const href = isFileUrl || isHttpUrl ? raw : (0, file_url_1.default)(abs, { resolve: true });
|
|
966
|
+
const base = abs
|
|
967
|
+
? path_1.default.basename(abs)
|
|
968
|
+
: isHttpUrl
|
|
969
|
+
? path_1.default.basename(new URL(raw).pathname) || artifact.name || artifact.title || 'attachment'
|
|
970
|
+
: artifact.name || artifact.title || 'attachment';
|
|
952
971
|
return {
|
|
953
972
|
...artifact,
|
|
954
973
|
name: artifact.name || artifact.title || base,
|