@testomatio/reporter 2.7.2-beta.1 → 2.7.3-beta.1-vitest
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 +31 -3
- package/lib/adapter/vitest.js +321 -51
- package/lib/client.d.ts +2 -1
- package/lib/client.js +3 -2
- package/package.json +1 -1
- package/src/adapter/vitest.js +319 -48
- package/src/client.js +3 -2
package/lib/adapter/vitest.d.ts
CHANGED
|
@@ -18,18 +18,46 @@ 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
|
})[];
|
|
26
|
+
_finalized: boolean;
|
|
27
|
+
_finalizing: boolean;
|
|
28
|
+
_runStartedAtMs: number;
|
|
29
|
+
_runStartedAtMicros: number;
|
|
30
|
+
_reportedTestKeys: Set<any>;
|
|
31
|
+
_liveQueue: Promise<void>;
|
|
27
32
|
onInit(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
35
|
+
*/
|
|
36
|
+
onTestRunStart(): void;
|
|
28
37
|
/**
|
|
29
38
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
30
39
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
31
40
|
*/
|
|
32
41
|
onFinished(files: VitestTestFile[] | undefined, errors: unknown[] | undefined): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Vitest 4+ reporter API callback.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array<unknown> | undefined} testModules
|
|
46
|
+
* @param {unknown[] | undefined} errors
|
|
47
|
+
*/
|
|
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>;
|
|
33
61
|
#private;
|
|
34
62
|
}
|
|
35
63
|
import { Client as TestomatioClient } from '../client.js';
|
package/lib/adapter/vitest.js
CHANGED
|
@@ -22,47 +22,119 @@ 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 = [];
|
|
27
|
+
this._finalized = false;
|
|
28
|
+
this._finalizing = false;
|
|
29
|
+
this._runStartedAtMs = null;
|
|
30
|
+
this._runStartedAtMicros = null;
|
|
31
|
+
this._reportedTestKeys = new Set();
|
|
32
|
+
this._liveQueue = Promise.resolve();
|
|
29
33
|
}
|
|
30
34
|
// on run start
|
|
31
35
|
onInit() {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
this._finalized = false;
|
|
38
|
+
this._finalizing = false;
|
|
39
|
+
this._runStartedAtMs = now;
|
|
40
|
+
this._runStartedAtMicros = now * 1000;
|
|
41
|
+
this._reportedTestKeys = new Set();
|
|
42
|
+
this._liveQueue = Promise.resolve();
|
|
32
43
|
this.client.createRun();
|
|
33
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
|
+
}
|
|
34
53
|
/**
|
|
35
54
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
36
55
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
37
56
|
*/
|
|
38
57
|
async onFinished(files, errors) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
if (this._finalized || this._finalizing)
|
|
59
|
+
return;
|
|
60
|
+
this._finalizing = true;
|
|
61
|
+
try {
|
|
62
|
+
this.tests = [];
|
|
63
|
+
if (!files || !files.length) {
|
|
64
|
+
console.info('No tests executed');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
files.forEach(file => {
|
|
68
|
+
// task could be test or suite
|
|
69
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
70
|
+
if (taskOrSuite.type === 'test') {
|
|
71
|
+
const test = taskOrSuite;
|
|
72
|
+
this.tests.push(this.#getDataFromTest(test));
|
|
73
|
+
}
|
|
74
|
+
else if (taskOrSuite.type === 'suite') {
|
|
75
|
+
const suite = taskOrSuite;
|
|
76
|
+
this.#processTasksOfSuite(suite);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error('Unprocessed case. Unknown task type');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
55
82
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
debug(this.tests.length, 'tests collected');
|
|
84
|
+
// send tests to Testomat.io
|
|
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);
|
|
90
|
+
await this.client.addTestRun(test.status, test);
|
|
91
|
+
}
|
|
92
|
+
await this._liveQueue;
|
|
93
|
+
console.log('finished');
|
|
94
|
+
if (errors.length)
|
|
95
|
+
console.error('Vitest adapter errors:', errors);
|
|
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
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this._finalizing = false;
|
|
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
|
+
* 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);
|
|
61
137
|
}
|
|
62
|
-
console.log('finished');
|
|
63
|
-
if (errors.length)
|
|
64
|
-
console.error('Vitest adapter errors:', errors);
|
|
65
|
-
await this.client.updateRunStatus(getRunStatusFromResults(files));
|
|
66
138
|
}
|
|
67
139
|
/* non-used listeners
|
|
68
140
|
onUserConsoleLog(log) {}
|
|
@@ -81,7 +153,7 @@ class VitestReporter {
|
|
|
81
153
|
* @param {VitestSuite} suite
|
|
82
154
|
*/
|
|
83
155
|
#processTasksOfSuite(suite) {
|
|
84
|
-
suite.
|
|
156
|
+
getTasks(suite).forEach(taskOrSuite => {
|
|
85
157
|
if (taskOrSuite.type === 'test') {
|
|
86
158
|
const test = taskOrSuite;
|
|
87
159
|
this.tests.push(this.#getDataFromTest(test));
|
|
@@ -98,25 +170,50 @@ class VitestReporter {
|
|
|
98
170
|
/**
|
|
99
171
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
100
172
|
*
|
|
101
|
-
* @param {
|
|
173
|
+
* @param {any} test
|
|
102
174
|
*
|
|
103
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
175
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
|
|
104
176
|
*/
|
|
105
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;
|
|
106
183
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
184
|
+
_reportKey: reportKey,
|
|
185
|
+
error: normalized.error,
|
|
186
|
+
file: normalized.file,
|
|
187
|
+
logs: normalized.logs,
|
|
188
|
+
meta: normalized.meta,
|
|
111
189
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
112
|
-
status: getTestStatus(
|
|
113
|
-
suite_title:
|
|
114
|
-
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(
|
|
115
|
-
time:
|
|
116
|
-
|
|
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,
|
|
117
196
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
118
197
|
};
|
|
119
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
|
+
}
|
|
120
217
|
}
|
|
121
218
|
exports.VitestReporter = VitestReporter;
|
|
122
219
|
/**
|
|
@@ -131,16 +228,15 @@ function getRunStatusFromResults(files) {
|
|
|
131
228
|
*/
|
|
132
229
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
133
230
|
files.forEach(file => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (taskOrSuite.result?.state === 'fail') {
|
|
231
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
232
|
+
if (isFailedState(taskOrSuite?.result?.state)) {
|
|
137
233
|
status = 'failed'; // set status to failed if any test failed
|
|
138
234
|
}
|
|
139
235
|
});
|
|
140
236
|
// if there are no failed tests > search for passed tests
|
|
141
237
|
if (status !== 'failed') {
|
|
142
|
-
file.
|
|
143
|
-
if (taskOrSuite
|
|
238
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
239
|
+
if (isPassedState(taskOrSuite?.result?.state)) {
|
|
144
240
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
145
241
|
}
|
|
146
242
|
});
|
|
@@ -151,17 +247,19 @@ function getRunStatusFromResults(files) {
|
|
|
151
247
|
/**
|
|
152
248
|
* Returns test status in Testomat.io format
|
|
153
249
|
*
|
|
154
|
-
* @param {
|
|
250
|
+
* @param {string | undefined} state
|
|
251
|
+
* @param {string | undefined} mode
|
|
155
252
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
156
253
|
*/
|
|
157
|
-
function getTestStatus(
|
|
158
|
-
if (
|
|
254
|
+
function getTestStatus(state, mode) {
|
|
255
|
+
if (isFailedState(state))
|
|
159
256
|
return constants_js_1.STATUS.FAILED;
|
|
160
|
-
if (
|
|
257
|
+
if (isPassedState(state))
|
|
161
258
|
return constants_js_1.STATUS.PASSED;
|
|
162
|
-
if (!
|
|
259
|
+
if (isSkippedState(state) || (!state && mode === 'skip'))
|
|
163
260
|
return constants_js_1.STATUS.SKIPPED;
|
|
164
|
-
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);
|
|
262
|
+
return constants_js_1.STATUS.SKIPPED;
|
|
165
263
|
}
|
|
166
264
|
/**
|
|
167
265
|
* @param {VitestTestLogs[]} logs
|
|
@@ -179,6 +277,178 @@ function transformLogsToString(logs) {
|
|
|
179
277
|
});
|
|
180
278
|
return logsStr;
|
|
181
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Supports both old and new Vitest task tree shapes.
|
|
282
|
+
*
|
|
283
|
+
* @param {any} node
|
|
284
|
+
* @returns {any[]}
|
|
285
|
+
*/
|
|
286
|
+
function getTasks(node) {
|
|
287
|
+
if (!node)
|
|
288
|
+
return [];
|
|
289
|
+
if (Array.isArray(node.tasks))
|
|
290
|
+
return node.tasks;
|
|
291
|
+
if (Array.isArray(node.children))
|
|
292
|
+
return node.children;
|
|
293
|
+
if (node.children && typeof node.children[Symbol.iterator] === 'function')
|
|
294
|
+
return Array.from(node.children);
|
|
295
|
+
if (node.task)
|
|
296
|
+
return [node.task];
|
|
297
|
+
return [];
|
|
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
|
+
}
|
|
182
452
|
module.exports = VitestReporter;
|
|
183
453
|
|
|
184
454
|
module.exports.VitestReporter = VitestReporter;
|
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(() => {
|
package/package.json
CHANGED
package/src/adapter/vitest.js
CHANGED
|
@@ -19,50 +19,122 @@ 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
|
-
/**
|
|
23
|
-
* @type {(TestData & {status: string})[]} tests
|
|
24
|
-
*/
|
|
22
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
25
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();
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
// on run start
|
|
29
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();
|
|
30
41
|
this.client.createRun();
|
|
31
42
|
}
|
|
32
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
|
+
|
|
33
53
|
/**
|
|
34
54
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
35
55
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
36
56
|
*/
|
|
37
57
|
async onFinished(files, errors) {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
});
|
|
52
81
|
});
|
|
53
|
-
});
|
|
54
82
|
|
|
55
|
-
|
|
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;
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
60
102
|
}
|
|
103
|
+
}
|
|
61
104
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|
|
64
117
|
|
|
65
|
-
|
|
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
|
+
}
|
|
66
138
|
}
|
|
67
139
|
|
|
68
140
|
/* non-used listeners
|
|
@@ -83,7 +155,7 @@ class VitestReporter {
|
|
|
83
155
|
* @param {VitestSuite} suite
|
|
84
156
|
*/
|
|
85
157
|
#processTasksOfSuite(suite) {
|
|
86
|
-
suite.
|
|
158
|
+
getTasks(suite).forEach(taskOrSuite => {
|
|
87
159
|
if (taskOrSuite.type === 'test') {
|
|
88
160
|
const test = taskOrSuite;
|
|
89
161
|
this.tests.push(this.#getDataFromTest(test));
|
|
@@ -99,25 +171,52 @@ class VitestReporter {
|
|
|
99
171
|
/**
|
|
100
172
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
101
173
|
*
|
|
102
|
-
* @param {
|
|
174
|
+
* @param {any} test
|
|
103
175
|
*
|
|
104
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
176
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
|
|
105
177
|
*/
|
|
106
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
|
+
|
|
107
186
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
187
|
+
_reportKey: reportKey,
|
|
188
|
+
error: normalized.error,
|
|
189
|
+
file: normalized.file,
|
|
190
|
+
logs: normalized.logs,
|
|
191
|
+
meta: normalized.meta,
|
|
112
192
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
113
|
-
status: getTestStatus(
|
|
114
|
-
suite_title:
|
|
115
|
-
test_id: getTestomatIdFromTestTitle(
|
|
116
|
-
time:
|
|
117
|
-
|
|
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,
|
|
118
199
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
119
200
|
};
|
|
120
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
|
+
}
|
|
121
220
|
}
|
|
122
221
|
|
|
123
222
|
/**
|
|
@@ -133,17 +232,16 @@ function getRunStatusFromResults(files) {
|
|
|
133
232
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
134
233
|
|
|
135
234
|
files.forEach(file => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (taskOrSuite.result?.state === 'fail') {
|
|
235
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
236
|
+
if (isFailedState(taskOrSuite?.result?.state)) {
|
|
139
237
|
status = 'failed'; // set status to failed if any test failed
|
|
140
238
|
}
|
|
141
239
|
});
|
|
142
240
|
|
|
143
241
|
// if there are no failed tests > search for passed tests
|
|
144
242
|
if (status !== 'failed') {
|
|
145
|
-
file.
|
|
146
|
-
if (taskOrSuite
|
|
243
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
244
|
+
if (isPassedState(taskOrSuite?.result?.state)) {
|
|
147
245
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
148
246
|
}
|
|
149
247
|
});
|
|
@@ -156,14 +254,16 @@ function getRunStatusFromResults(files) {
|
|
|
156
254
|
/**
|
|
157
255
|
* Returns test status in Testomat.io format
|
|
158
256
|
*
|
|
159
|
-
* @param {
|
|
257
|
+
* @param {string | undefined} state
|
|
258
|
+
* @param {string | undefined} mode
|
|
160
259
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
161
260
|
*/
|
|
162
|
-
function getTestStatus(
|
|
163
|
-
if (
|
|
164
|
-
if (
|
|
165
|
-
if (!
|
|
166
|
-
console.error(pc.red('Unprocessed case for defining test status. Contact dev team.
|
|
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;
|
|
167
267
|
}
|
|
168
268
|
|
|
169
269
|
/**
|
|
@@ -180,5 +280,176 @@ function transformLogsToString(logs) {
|
|
|
180
280
|
return logsStr;
|
|
181
281
|
}
|
|
182
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
|
+
|
|
183
454
|
export default VitestReporter;
|
|
184
455
|
export { VitestReporter };
|
package/src/client.js
CHANGED
|
@@ -360,10 +360,11 @@ 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).
|
|
363
364
|
* Must be one of "passed", "failed", or "finished"
|
|
364
365
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
365
366
|
*/
|
|
366
|
-
async updateRunStatus(status) {
|
|
367
|
+
async updateRunStatus(status, params = {}) {
|
|
367
368
|
this.pipes ||= await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
368
369
|
this.runId ||= readLatestRunId();
|
|
369
370
|
|
|
@@ -371,7 +372,7 @@ class Client {
|
|
|
371
372
|
// all pipes disabled, skipping
|
|
372
373
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
373
374
|
|
|
374
|
-
const runParams = { status };
|
|
375
|
+
const runParams = { ...params, status };
|
|
375
376
|
|
|
376
377
|
this.queue = this.queue
|
|
377
378
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|