@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.
@@ -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
- /** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
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';
@@ -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
- /** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
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
- 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 });
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 {any} test
129
+ * @param {VitestTest} test
174
130
  *
175
- * @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
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
- _reportKey: reportKey,
185
- error: normalized.error,
186
- file: normalized.file,
187
- logs: normalized.logs,
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(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,
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
- getTasks(file).forEach(taskOrSuite => {
232
- if (isFailedState(taskOrSuite?.result?.state)) {
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
- getTasks(file).forEach(taskOrSuite => {
239
- if (isPassedState(taskOrSuite?.result?.state)) {
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 {string | undefined} state
251
- * @param {string | undefined} mode
182
+ * @param {VitestTest} test
252
183
  * @returns 'passed' | 'failed' | 'skipped'
253
184
  */
254
- function getTestStatus(state, mode) {
255
- if (isFailedState(state))
185
+ function getTestStatus(test) {
186
+ if (test.result?.state === 'fail')
256
187
  return constants_js_1.STATUS.FAILED;
257
- if (isPassedState(state))
188
+ if (test.result?.state === 'pass')
258
189
  return constants_js_1.STATUS.PASSED;
259
- if (isSkippedState(state) || (!state && mode === 'skip'))
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. State:'), state);
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", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
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, steps, logs: testData.logs });
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, params = {}) {
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 = { ...params, status };
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
- const link = artifact?.link || artifact?.path;
184
- return link && (link.startsWith('http://') || link.startsWith('https://')) && !link.startsWith('file://');
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: any[] | any;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.7.3-beta.1-vitest",
3
+ "version": "2.7.3",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -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
- /** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
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
- 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
+ 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 {any} test
131
+ * @param {VitestTest} test
175
132
  *
176
- * @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
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
- _reportKey: reportKey,
188
- error: normalized.error,
189
- file: normalized.file,
190
- logs: normalized.logs,
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(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,
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
- getTasks(file).forEach(taskOrSuite => {
236
- if (isFailedState(taskOrSuite?.result?.state)) {
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
- getTasks(file).forEach(taskOrSuite => {
244
- if (isPassedState(taskOrSuite?.result?.state)) {
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 {string | undefined} state
258
- * @param {string | undefined} mode
188
+ * @param {VitestTest} test
259
189
  * @returns 'passed' | 'failed' | 'skipped'
260
190
  */
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);
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, steps, logs: testData.logs });
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, params = {}) {
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 = { ...params, status };
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
- const link = artifact?.link || artifact?.path;
234
- return link && (link.startsWith('http://') || link.startsWith('https://')) && !link.startsWith('file://');
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
  */