@testomatio/reporter 2.7.3-beta.1-vitest → 2.7.3
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/lib/adapter/vitest.d.ts +3 -22
- package/lib/adapter/vitest.js +26 -250
- package/lib/client.d.ts +1 -2
- package/lib/client.js +3 -4
- package/lib/pipe/html.js +6 -2
- package/lib/utils/log-formatter.d.ts +2 -2
- package/lib/utils/log-formatter.js +1 -1
- package/package.json +1 -1
- package/src/adapter/vitest.js +26 -253
- package/src/client.js +3 -4
- package/src/pipe/html.js +4 -2
- package/src/utils/log-formatter.js +1 -1
package/lib/adapter/vitest.d.ts
CHANGED
|
@@ -18,22 +18,15 @@ export type TestData = import("../../types/types.js").TestData;
|
|
|
18
18
|
export class VitestReporter {
|
|
19
19
|
constructor(config?: {});
|
|
20
20
|
client: TestomatioClient;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* @type {(TestData & {status: string})[]} tests
|
|
23
|
+
*/
|
|
22
24
|
tests: (TestData & {
|
|
23
25
|
status: string;
|
|
24
|
-
_reportKey?: string | null;
|
|
25
26
|
})[];
|
|
26
27
|
_finalized: boolean;
|
|
27
28
|
_finalizing: boolean;
|
|
28
|
-
_runStartedAtMs: number;
|
|
29
|
-
_runStartedAtMicros: number;
|
|
30
|
-
_reportedTestKeys: Set<any>;
|
|
31
|
-
_liveQueue: Promise<void>;
|
|
32
29
|
onInit(): void;
|
|
33
|
-
/**
|
|
34
|
-
* Vitest 3/4 callback fired when test run starts.
|
|
35
|
-
*/
|
|
36
|
-
onTestRunStart(): void;
|
|
37
30
|
/**
|
|
38
31
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
39
32
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -46,18 +39,6 @@ export class VitestReporter {
|
|
|
46
39
|
* @param {unknown[] | undefined} errors
|
|
47
40
|
*/
|
|
48
41
|
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>;
|
|
61
42
|
#private;
|
|
62
43
|
}
|
|
63
44
|
import { Client as TestomatioClient } from '../client.js';
|
package/lib/adapter/vitest.js
CHANGED
|
@@ -22,34 +22,19 @@ 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
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* @type {(TestData & {status: string})[]} tests
|
|
27
|
+
*/
|
|
26
28
|
this.tests = [];
|
|
27
29
|
this._finalized = false;
|
|
28
30
|
this._finalizing = false;
|
|
29
|
-
this._runStartedAtMs = null;
|
|
30
|
-
this._runStartedAtMicros = null;
|
|
31
|
-
this._reportedTestKeys = new Set();
|
|
32
|
-
this._liveQueue = Promise.resolve();
|
|
33
31
|
}
|
|
34
32
|
// on run start
|
|
35
33
|
onInit() {
|
|
36
|
-
const now = Date.now();
|
|
37
34
|
this._finalized = false;
|
|
38
35
|
this._finalizing = false;
|
|
39
|
-
this._runStartedAtMs = now;
|
|
40
|
-
this._runStartedAtMicros = now * 1000;
|
|
41
|
-
this._reportedTestKeys = new Set();
|
|
42
|
-
this._liveQueue = Promise.resolve();
|
|
43
36
|
this.client.createRun();
|
|
44
37
|
}
|
|
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
|
-
}
|
|
53
38
|
/**
|
|
54
39
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
55
40
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -83,19 +68,12 @@ class VitestReporter {
|
|
|
83
68
|
debug(this.tests.length, 'tests collected');
|
|
84
69
|
// send tests to Testomat.io
|
|
85
70
|
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);
|
|
90
71
|
await this.client.addTestRun(test.status, test);
|
|
91
72
|
}
|
|
92
|
-
await this._liveQueue;
|
|
93
73
|
console.log('finished');
|
|
94
74
|
if (errors.length)
|
|
95
75
|
console.error('Vitest adapter errors:', errors);
|
|
96
|
-
|
|
97
|
-
const duration = Math.max(0, (Date.now() - startedAtMs) / 1000);
|
|
98
|
-
await this.client.updateRunStatus(getRunStatusFromResults(files), { duration });
|
|
76
|
+
await this.client.updateRunStatus(getRunStatusFromResults(files));
|
|
99
77
|
this._finalized = true;
|
|
100
78
|
}
|
|
101
79
|
finally {
|
|
@@ -114,28 +92,6 @@ class VitestReporter {
|
|
|
114
92
|
.filter(Boolean);
|
|
115
93
|
await this.onFinished(files, errors);
|
|
116
94
|
}
|
|
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
|
-
}
|
|
139
95
|
/* non-used listeners
|
|
140
96
|
onUserConsoleLog(log) {}
|
|
141
97
|
onPathsCollected(paths) {} // paths array to files with tests
|
|
@@ -170,50 +126,25 @@ class VitestReporter {
|
|
|
170
126
|
/**
|
|
171
127
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
172
128
|
*
|
|
173
|
-
* @param {
|
|
129
|
+
* @param {VitestTest} test
|
|
174
130
|
*
|
|
175
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'
|
|
131
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
176
132
|
*/
|
|
177
133
|
#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;
|
|
183
134
|
return {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
meta: normalized.meta,
|
|
135
|
+
error: test.result?.errors ? test.result.errors[0] : undefined,
|
|
136
|
+
file: test.file?.name || test.file?.filepath || '',
|
|
137
|
+
logs: test.logs ? transformLogsToString(test.logs) : '',
|
|
138
|
+
meta: test.meta,
|
|
189
139
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
190
|
-
status: getTestStatus(
|
|
191
|
-
suite_title:
|
|
192
|
-
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(
|
|
193
|
-
time:
|
|
194
|
-
|
|
195
|
-
title: normalized.name,
|
|
140
|
+
status: getTestStatus(test),
|
|
141
|
+
suite_title: test.suite?.name || test.file?.name || test.file?.filepath,
|
|
142
|
+
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(test.name),
|
|
143
|
+
time: test.result?.duration || 0,
|
|
144
|
+
title: test.name,
|
|
196
145
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
197
146
|
};
|
|
198
147
|
}
|
|
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
|
-
}
|
|
217
148
|
}
|
|
218
149
|
exports.VitestReporter = VitestReporter;
|
|
219
150
|
/**
|
|
@@ -228,15 +159,16 @@ function getRunStatusFromResults(files) {
|
|
|
228
159
|
*/
|
|
229
160
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
230
161
|
files.forEach(file => {
|
|
231
|
-
|
|
232
|
-
|
|
162
|
+
// search for failed tests
|
|
163
|
+
file.tasks.forEach(taskOrSuite => {
|
|
164
|
+
if (taskOrSuite.result?.state === 'fail') {
|
|
233
165
|
status = 'failed'; // set status to failed if any test failed
|
|
234
166
|
}
|
|
235
167
|
});
|
|
236
168
|
// if there are no failed tests > search for passed tests
|
|
237
169
|
if (status !== 'failed') {
|
|
238
|
-
|
|
239
|
-
if (
|
|
170
|
+
file.tasks.forEach(taskOrSuite => {
|
|
171
|
+
if (taskOrSuite.result?.state === 'pass') {
|
|
240
172
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
241
173
|
}
|
|
242
174
|
});
|
|
@@ -247,18 +179,17 @@ function getRunStatusFromResults(files) {
|
|
|
247
179
|
/**
|
|
248
180
|
* Returns test status in Testomat.io format
|
|
249
181
|
*
|
|
250
|
-
* @param {
|
|
251
|
-
* @param {string | undefined} mode
|
|
182
|
+
* @param {VitestTest} test
|
|
252
183
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
253
184
|
*/
|
|
254
|
-
function getTestStatus(
|
|
255
|
-
if (
|
|
185
|
+
function getTestStatus(test) {
|
|
186
|
+
if (test.result?.state === 'fail')
|
|
256
187
|
return constants_js_1.STATUS.FAILED;
|
|
257
|
-
if (
|
|
188
|
+
if (test.result?.state === 'pass')
|
|
258
189
|
return constants_js_1.STATUS.PASSED;
|
|
259
|
-
if (
|
|
190
|
+
if (test.result?.state === 'skip' || (!test.result && test.mode === 'skip'))
|
|
260
191
|
return constants_js_1.STATUS.SKIPPED;
|
|
261
|
-
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team.
|
|
192
|
+
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team. Test:'), test);
|
|
262
193
|
return constants_js_1.STATUS.SKIPPED;
|
|
263
194
|
}
|
|
264
195
|
/**
|
|
@@ -290,165 +221,10 @@ function getTasks(node) {
|
|
|
290
221
|
return node.tasks;
|
|
291
222
|
if (Array.isArray(node.children))
|
|
292
223
|
return node.children;
|
|
293
|
-
if (node.children && typeof node.children[Symbol.iterator] === 'function')
|
|
294
|
-
return Array.from(node.children);
|
|
295
224
|
if (node.task)
|
|
296
225
|
return [node.task];
|
|
297
226
|
return [];
|
|
298
227
|
}
|
|
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
|
-
}
|
|
452
228
|
module.exports = VitestReporter;
|
|
453
229
|
|
|
454
230
|
module.exports.VitestReporter = VitestReporter;
|
package/lib/client.d.ts
CHANGED
|
@@ -65,10 +65,9 @@ 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).
|
|
69
68
|
* Must be one of "passed", "failed", or "finished"
|
|
70
69
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
71
70
|
*/
|
|
72
|
-
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"
|
|
71
|
+
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
|
|
73
72
|
}
|
|
74
73
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -223,7 +223,7 @@ class Client {
|
|
|
223
223
|
errorFormatted += (0, log_formatter_js_1.formatError)(error) || '';
|
|
224
224
|
message = error?.message;
|
|
225
225
|
}
|
|
226
|
-
let fullLogs = (0, log_formatter_js_1.formatLogs)({ error: errorFormatted,
|
|
226
|
+
let fullLogs = (0, log_formatter_js_1.formatLogs)({ error: errorFormatted, logs: testData.logs });
|
|
227
227
|
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
228
228
|
uploadedFiles.push(this.uploader.uploadFileAsBuffer(Buffer.from((0, log_formatter_js_1.stripColors)(fullLogs), 'utf8'), [
|
|
229
229
|
this.runId,
|
|
@@ -308,18 +308,17 @@ 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).
|
|
312
311
|
* Must be one of "passed", "failed", or "finished"
|
|
313
312
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
314
313
|
*/
|
|
315
|
-
async updateRunStatus(status
|
|
314
|
+
async updateRunStatus(status) {
|
|
316
315
|
this.pipes ||= await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
317
316
|
this.runId ||= (0, utils_js_1.readLatestRunId)();
|
|
318
317
|
debug('Updating run status...');
|
|
319
318
|
// all pipes disabled, skipping
|
|
320
319
|
if (!this.pipes?.filter(p => p.isEnabled).length)
|
|
321
320
|
return Promise.resolve();
|
|
322
|
-
const runParams = {
|
|
321
|
+
const runParams = { status };
|
|
323
322
|
this.queue = this.queue
|
|
324
323
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
325
324
|
.then(() => {
|
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) {
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
3
3
|
* @param {Object} params - Parameters for formatting logs
|
|
4
4
|
* @param {string} params.error - Error message
|
|
5
|
-
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
5
|
+
* @param {Array|any} [params.steps] - Test steps (array or other types)
|
|
6
6
|
* @param {string} params.logs - Test logs
|
|
7
7
|
* @returns {string}
|
|
8
8
|
*/
|
|
9
9
|
export function formatLogs({ error, steps, logs }: {
|
|
10
10
|
error: string;
|
|
11
|
-
steps
|
|
11
|
+
steps?: any[] | any;
|
|
12
12
|
logs: string;
|
|
13
13
|
}): string;
|
|
14
14
|
/**
|
|
@@ -18,7 +18,7 @@ exports.stripColors = stripColors;
|
|
|
18
18
|
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
19
19
|
* @param {Object} params - Parameters for formatting logs
|
|
20
20
|
* @param {string} params.error - Error message
|
|
21
|
-
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
21
|
+
* @param {Array|any} [params.steps] - Test steps (array or other types)
|
|
22
22
|
* @param {string} params.logs - Test logs
|
|
23
23
|
* @returns {string}
|
|
24
24
|
*/
|
package/package.json
CHANGED
package/src/adapter/vitest.js
CHANGED
|
@@ -19,37 +19,21 @@ const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
|
|
|
19
19
|
class VitestReporter {
|
|
20
20
|
constructor(config = {}) {
|
|
21
21
|
this.client = new TestomatioClient({ apiKey: config?.apiKey });
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* @type {(TestData & {status: string})[]} tests
|
|
24
|
+
*/
|
|
23
25
|
this.tests = [];
|
|
24
26
|
this._finalized = false;
|
|
25
27
|
this._finalizing = false;
|
|
26
|
-
this._runStartedAtMs = null;
|
|
27
|
-
this._runStartedAtMicros = null;
|
|
28
|
-
this._reportedTestKeys = new Set();
|
|
29
|
-
this._liveQueue = Promise.resolve();
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// on run start
|
|
33
31
|
onInit() {
|
|
34
|
-
const now = Date.now();
|
|
35
32
|
this._finalized = false;
|
|
36
33
|
this._finalizing = false;
|
|
37
|
-
this._runStartedAtMs = now;
|
|
38
|
-
this._runStartedAtMicros = now * 1000;
|
|
39
|
-
this._reportedTestKeys = new Set();
|
|
40
|
-
this._liveQueue = Promise.resolve();
|
|
41
34
|
this.client.createRun();
|
|
42
35
|
}
|
|
43
36
|
|
|
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
37
|
/**
|
|
54
38
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
55
39
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -84,18 +68,13 @@ class VitestReporter {
|
|
|
84
68
|
|
|
85
69
|
// send tests to Testomat.io
|
|
86
70
|
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
71
|
await this.client.addTestRun(test.status, test);
|
|
90
72
|
}
|
|
91
|
-
await this._liveQueue;
|
|
92
73
|
|
|
93
74
|
console.log('finished');
|
|
94
75
|
if (errors.length) console.error('Vitest adapter errors:', errors);
|
|
95
76
|
|
|
96
|
-
|
|
97
|
-
const duration = Math.max(0, (Date.now() - startedAtMs) / 1000);
|
|
98
|
-
await this.client.updateRunStatus(getRunStatusFromResults(files), { duration });
|
|
77
|
+
await this.client.updateRunStatus(getRunStatusFromResults(files));
|
|
99
78
|
this._finalized = true;
|
|
100
79
|
} finally {
|
|
101
80
|
this._finalizing = false;
|
|
@@ -115,28 +94,6 @@ class VitestReporter {
|
|
|
115
94
|
await this.onFinished(files, errors);
|
|
116
95
|
}
|
|
117
96
|
|
|
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
97
|
/* non-used listeners
|
|
141
98
|
onUserConsoleLog(log) {}
|
|
142
99
|
onPathsCollected(paths) {} // paths array to files with tests
|
|
@@ -171,52 +128,25 @@ class VitestReporter {
|
|
|
171
128
|
/**
|
|
172
129
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
173
130
|
*
|
|
174
|
-
* @param {
|
|
131
|
+
* @param {VitestTest} test
|
|
175
132
|
*
|
|
176
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'
|
|
133
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
177
134
|
*/
|
|
178
135
|
#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
136
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
meta: normalized.meta,
|
|
137
|
+
error: test.result?.errors ? test.result.errors[0] : undefined,
|
|
138
|
+
file: test.file?.name || test.file?.filepath || '',
|
|
139
|
+
logs: test.logs ? transformLogsToString(test.logs) : '',
|
|
140
|
+
meta: test.meta,
|
|
192
141
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
193
|
-
status: getTestStatus(
|
|
194
|
-
suite_title:
|
|
195
|
-
test_id: getTestomatIdFromTestTitle(
|
|
196
|
-
time:
|
|
197
|
-
|
|
198
|
-
title: normalized.name,
|
|
142
|
+
status: getTestStatus(test),
|
|
143
|
+
suite_title: test.suite?.name || test.file?.name || test.file?.filepath,
|
|
144
|
+
test_id: getTestomatIdFromTestTitle(test.name),
|
|
145
|
+
time: test.result?.duration || 0,
|
|
146
|
+
title: test.name,
|
|
199
147
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
200
148
|
};
|
|
201
149
|
}
|
|
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
150
|
}
|
|
221
151
|
|
|
222
152
|
/**
|
|
@@ -232,16 +162,17 @@ function getRunStatusFromResults(files) {
|
|
|
232
162
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
233
163
|
|
|
234
164
|
files.forEach(file => {
|
|
235
|
-
|
|
236
|
-
|
|
165
|
+
// search for failed tests
|
|
166
|
+
file.tasks.forEach(taskOrSuite => {
|
|
167
|
+
if (taskOrSuite.result?.state === 'fail') {
|
|
237
168
|
status = 'failed'; // set status to failed if any test failed
|
|
238
169
|
}
|
|
239
170
|
});
|
|
240
171
|
|
|
241
172
|
// if there are no failed tests > search for passed tests
|
|
242
173
|
if (status !== 'failed') {
|
|
243
|
-
|
|
244
|
-
if (
|
|
174
|
+
file.tasks.forEach(taskOrSuite => {
|
|
175
|
+
if (taskOrSuite.result?.state === 'pass') {
|
|
245
176
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
246
177
|
}
|
|
247
178
|
});
|
|
@@ -254,15 +185,14 @@ function getRunStatusFromResults(files) {
|
|
|
254
185
|
/**
|
|
255
186
|
* Returns test status in Testomat.io format
|
|
256
187
|
*
|
|
257
|
-
* @param {
|
|
258
|
-
* @param {string | undefined} mode
|
|
188
|
+
* @param {VitestTest} test
|
|
259
189
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
260
190
|
*/
|
|
261
|
-
function getTestStatus(
|
|
262
|
-
if (
|
|
263
|
-
if (
|
|
264
|
-
if (
|
|
265
|
-
console.error(pc.red('Unprocessed case for defining test status. Contact dev team.
|
|
191
|
+
function getTestStatus(test) {
|
|
192
|
+
if (test.result?.state === 'fail') return STATUS.FAILED;
|
|
193
|
+
if (test.result?.state === 'pass') return STATUS.PASSED;
|
|
194
|
+
if (test.result?.state === 'skip' || (!test.result && test.mode === 'skip')) return STATUS.SKIPPED;
|
|
195
|
+
console.error(pc.red('Unprocessed case for defining test status. Contact dev team. Test:'), test);
|
|
266
196
|
return STATUS.SKIPPED;
|
|
267
197
|
}
|
|
268
198
|
|
|
@@ -290,166 +220,9 @@ function getTasks(node) {
|
|
|
290
220
|
if (!node) return [];
|
|
291
221
|
if (Array.isArray(node.tasks)) return node.tasks;
|
|
292
222
|
if (Array.isArray(node.children)) return node.children;
|
|
293
|
-
if (node.children && typeof node.children[Symbol.iterator] === 'function') return Array.from(node.children);
|
|
294
223
|
if (node.task) return [node.task];
|
|
295
224
|
return [];
|
|
296
225
|
}
|
|
297
226
|
|
|
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
227
|
export default VitestReporter;
|
|
455
228
|
export { VitestReporter };
|
package/src/client.js
CHANGED
|
@@ -257,7 +257,7 @@ class Client {
|
|
|
257
257
|
message = error?.message;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
let fullLogs = formatLogs({ error: errorFormatted,
|
|
260
|
+
let fullLogs = formatLogs({ error: errorFormatted, logs: testData.logs });
|
|
261
261
|
|
|
262
262
|
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
263
263
|
uploadedFiles.push(
|
|
@@ -360,11 +360,10 @@ class Client {
|
|
|
360
360
|
*
|
|
361
361
|
* Updates the status of the current test run and finishes the run.
|
|
362
362
|
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
363
|
-
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
364
363
|
* Must be one of "passed", "failed", or "finished"
|
|
365
364
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
366
365
|
*/
|
|
367
|
-
async updateRunStatus(status
|
|
366
|
+
async updateRunStatus(status) {
|
|
368
367
|
this.pipes ||= await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
369
368
|
this.runId ||= readLatestRunId();
|
|
370
369
|
|
|
@@ -372,7 +371,7 @@ class Client {
|
|
|
372
371
|
// all pipes disabled, skipping
|
|
373
372
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
374
373
|
|
|
375
|
-
const runParams = {
|
|
374
|
+
const runParams = { status };
|
|
376
375
|
|
|
377
376
|
this.queue = this.queue
|
|
378
377
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
package/src/pipe/html.js
CHANGED
|
@@ -230,8 +230,10 @@ class HtmlPipe {
|
|
|
230
230
|
];
|
|
231
231
|
|
|
232
232
|
test.artifactsUploaded = allPossibleArtifacts.some(artifact => {
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
let link = artifact?.link || artifact?.path;
|
|
234
|
+
if (!link) return false;
|
|
235
|
+
if (typeof link !== 'string') link = String(link);
|
|
236
|
+
return link.startsWith('http://') || link.startsWith('https://');
|
|
235
237
|
});
|
|
236
238
|
|
|
237
239
|
normalizeRetries(test);
|
|
@@ -11,7 +11,7 @@ const stripColors = stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9
|
|
|
11
11
|
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
12
12
|
* @param {Object} params - Parameters for formatting logs
|
|
13
13
|
* @param {string} params.error - Error message
|
|
14
|
-
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
14
|
+
* @param {Array|any} [params.steps] - Test steps (array or other types)
|
|
15
15
|
* @param {string} params.logs - Test logs
|
|
16
16
|
* @returns {string}
|
|
17
17
|
*/
|