@testomatio/reporter 1.4.5 → 1.4.6

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.
@@ -0,0 +1,182 @@
1
+ const chalk = require('chalk');
2
+ const { TestomatioClient } = require('../client');
3
+ const { STATUS } = require('../constants');
4
+ const { getTestomatIdFromTestTitle } = require('../utils/utils');
5
+ const debug = require('debug')('@testomatio/reporter:adapter:vitest');
6
+
7
+ /**
8
+ * @typedef {import('../../types').VitestTest} VitestTest
9
+ * @typedef {import('../../types').VitestTestFile} VitestTestFile
10
+ * @typedef {import('../../types').VitestSuite} VitestSuite
11
+ * @typedef {import('../../types').VitestTestLogs} VitestTestLogs
12
+ * @typedef {import('../../vitest.types').ErrorWithDiff} ErrorWithDiff
13
+ * @typedef {typeof import('../constants').STATUS} STATUS
14
+ * @typedef {import('../../types').TestData} TestData
15
+ */
16
+
17
+ class VitestReporter {
18
+ constructor(config = {}) {
19
+ this.client = new TestomatioClient({ apiKey: config?.apiKey });
20
+ /**
21
+ * @type {(TestData & {status: string})[]} tests
22
+ */
23
+ this.tests = [];
24
+ }
25
+
26
+ // on run start
27
+ onInit() {
28
+ this.client.createRun();
29
+ }
30
+
31
+ /**
32
+ * @param {VitestTestFile[] | undefined} files // array with results;
33
+ * @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
34
+ */
35
+ async onFinished(files, errors) {
36
+ if (!files || !files.length) console.info('No tests executed');
37
+
38
+ files.forEach(file => {
39
+ // task could be test or suite
40
+ file.tasks.forEach(taskOrSuite => {
41
+ if (taskOrSuite.type === 'test') {
42
+ const test = taskOrSuite;
43
+ this.tests.push(this.#getDataFromTest(test));
44
+ } else if (taskOrSuite.type === 'suite') {
45
+ const suite = taskOrSuite;
46
+ this.#processTasksOfSuite(suite);
47
+ } else {
48
+ throw new Error('Unprocessed case. Unknown task type');
49
+ }
50
+ });
51
+ });
52
+
53
+ debug(this.tests.length, 'tests collected');
54
+
55
+ // send tests to Testomat.io
56
+ for (const test of this.tests) {
57
+ await this.client.addTestRun(test.status, test);
58
+ }
59
+
60
+ console.log('finished');
61
+ if (errors.length) console.error('Vitest adapter errors:', errors);
62
+
63
+ await this.client.updateRunStatus(getRunStatusFromResults(files));
64
+ }
65
+
66
+ /* non-used listeners
67
+ onUserConsoleLog(log) {}
68
+ onPathsCollected(paths) {} // paths array to files with tests
69
+ onCollected(files) {} // files array with tests (but without results)
70
+ onTaskUpdate(packs) {} // some updates come here on afterAll block execution
71
+ onTestRemoved(trigger) {}
72
+ onWatcherStart(files, errors) {}
73
+ onWatcherRerun(files, trigger) {}
74
+ onServerRestart(reason) {}
75
+ onProcessTimeout() {}
76
+ */
77
+
78
+ /**
79
+ * Recursively gets all tasks from suite and pushes them to "tests" array
80
+ *
81
+ * @param {VitestSuite} suite
82
+ */
83
+ #processTasksOfSuite(suite) {
84
+ suite.tasks.forEach(taskOrSuite => {
85
+ if (taskOrSuite.type === 'test') {
86
+ const test = taskOrSuite;
87
+ this.tests.push(this.#getDataFromTest(test));
88
+ } else if (taskOrSuite.type === 'suite') {
89
+ const theSuite = taskOrSuite;
90
+ this.#processTasksOfSuite(theSuite);
91
+ } else {
92
+ throw new Error('Unprocessed case. Unknown task type');
93
+ }
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Processes task and returns test data ready to be sent to Testomat.io
99
+ *
100
+ * @param {VitestTest} test
101
+ *
102
+ * @returns {TestData & {status: string}}
103
+ */
104
+ #getDataFromTest(test) {
105
+ return {
106
+ error: test.result?.errors ? test.result.errors[0] : undefined,
107
+ file: test.file.name,
108
+ logs: test.logs ? transformLogsToString(test.logs) : '',
109
+ meta: test.meta,
110
+ status: getTestStatus(test),
111
+ suite_title: test.suite.name || test.file?.name,
112
+ test_id: getTestomatIdFromTestTitle(test.name),
113
+ time: test.result?.duration || 0,
114
+ title: test.name,
115
+ // testomatio functions (artifacts, logs, steps, meta) are not supported
116
+ };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Returns run status based on test results
122
+ *
123
+ * @param {VitestTestFile[]} files
124
+ * @returns {'passed' | 'failed' | 'finished'}
125
+ */
126
+ function getRunStatusFromResults(files) {
127
+ /**
128
+ * @type {'passed' | 'failed' | 'finished'}
129
+ */
130
+ let status = 'finished'; // default status (if no failed or passed tests)
131
+
132
+ files.forEach(file => {
133
+ // search for failed tests
134
+ file.tasks.forEach(taskOrSuite => {
135
+ if (taskOrSuite.result?.state === 'fail') {
136
+ status = 'failed'; // set status to failed if any test failed
137
+ }
138
+ });
139
+
140
+ // if there are no failed tests > search for passed tests
141
+ if (status !== 'failed') {
142
+ file.tasks.forEach(taskOrSuite => {
143
+ if (taskOrSuite.result?.state === 'pass') {
144
+ status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
145
+ }
146
+ });
147
+ }
148
+ });
149
+
150
+ return status;
151
+ }
152
+
153
+ /**
154
+ * Returns test status in Testomat.io format
155
+ *
156
+ * @param {VitestTest} test
157
+ * @returns 'passed' | 'failed' | 'skipped'
158
+ */
159
+ function getTestStatus(test) {
160
+ if (test.result?.state === 'fail') return STATUS.FAILED;
161
+ if (test.result?.state === 'pass') return STATUS.PASSED;
162
+ if (!test.result && test.mode === 'skip') return STATUS.SKIPPED;
163
+ console.error(chalk.red('Unprocessed case for defining test status. Contact dev team. Test:'), test);
164
+ }
165
+
166
+ /**
167
+ * @param {VitestTestLogs[]} logs
168
+ * @returns string
169
+ */
170
+ function transformLogsToString(logs) {
171
+ if (!logs) return '';
172
+ let logsStr = '';
173
+ logs.forEach(log => {
174
+ if (log.type === 'stdout') logsStr += `${log.content}\n`;
175
+ if (log.type === 'stderr') logsStr += `${chalk.red(log.content)}\n`;
176
+ });
177
+ return logsStr;
178
+ }
179
+
180
+ module.exports.VitestReporter = VitestReporter;
181
+ module.exports.default = VitestReporter;
182
+ module.exports = VitestReporter;
@@ -44,10 +44,15 @@ program
44
44
 
45
45
  let timeoutTimer;
46
46
  if (opts.timelimit) {
47
- timeoutTimer = setTimeout(() => {
48
- console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
49
- process.exit(0);
50
- }, parseInt(opts.timelimit, 10) * 1000);
47
+ timeoutTimer = setTimeout(
48
+ () => {
49
+ console.log(
50
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
51
+ );
52
+ process.exit(0);
53
+ },
54
+ parseInt(opts.timelimit, 10) * 1000,
55
+ );
51
56
  }
52
57
 
53
58
  try {
package/lib/client.js CHANGED
@@ -123,7 +123,7 @@ class Client {
123
123
  if (isTestShouldBeExculedFromReport(testData)) return [];
124
124
 
125
125
  if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
126
- debug('Skipping test from report', testData?.title)
126
+ debug('Skipping test from report', testData?.title);
127
127
  return []; // do not log skipped tests
128
128
  }
129
129
 
@@ -133,10 +133,13 @@ class Client {
133
133
  suite_title: 'Unknown suite',
134
134
  };
135
135
 
136
+ /**
137
+ * @type {TestData}
138
+ */
136
139
  const {
137
140
  rid,
138
141
  error = null,
139
- time = '',
142
+ time = 0,
140
143
  example = null,
141
144
  files = [],
142
145
  filesBuffers = [],
@@ -198,7 +201,7 @@ class Client {
198
201
  suite_id,
199
202
  test_id,
200
203
  message,
201
- run_time: parseFloat(time),
204
+ run_time: typeof time === 'number' ? time : parseFloat(time),
202
205
  artifacts,
203
206
  meta,
204
207
  };
@@ -224,7 +227,8 @@ class Client {
224
227
  /**
225
228
  *
226
229
  * Updates the status of the current test run and finishes the run.
227
- * @param {RunStatus} status - The status of the current test run. Must be one of "passed", "failed", or "finished"
230
+ * @param {'passed' | 'failed' | 'finished'} status - The status of the current test run.
231
+ * Must be one of "passed", "failed", or "finished"
228
232
  * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
229
233
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
230
234
  */
@@ -286,13 +290,23 @@ class Client {
286
290
  if (!message) message = error.message;
287
291
  if (error.inspect) message = error.inspect() || '';
288
292
 
289
- let stack = `${message}\n`;
293
+ let stack = '';
294
+ if (error.name) stack += `${chalk.red(error.name)}`;
295
+ if (error.operator) stack += ` (${chalk.red(error.operator)})`;
296
+ // add new line if something was added to stack
297
+ if (stack) stack += ': ';
290
298
 
291
- // diffs for mocha, cypress, codeceptjs style
292
- if (error.actual && error.expected) {
299
+ stack += `${message}\n`;
300
+
301
+ if (error.diff) {
302
+ // diff for vitest
303
+ stack += error.diff;
304
+ stack += '\n\n';
305
+ } else if (error.actual && error.expected && error.actual !== error.expected) {
306
+ // diffs for mocha, cypress, codeceptjs style
293
307
  stack += `\n\n${chalk.bold.green('+ expected')} ${chalk.bold.red('- actual')}`;
294
- stack += `\n${chalk.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
295
308
  stack += `\n${chalk.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
309
+ stack += `\n${chalk.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
296
310
  stack += '\n\n';
297
311
  }
298
312
 
@@ -365,3 +379,4 @@ function isTestShouldBeExculedFromReport(testData) {
365
379
  }
366
380
 
367
381
  module.exports = Client;
382
+ module.exports.TestomatioClient = Client;
@@ -79,13 +79,13 @@ class GitLabPipe {
79
79
  let summary = `${this.hiddenCommentData}
80
80
 
81
81
  | [![Testomat.io Report](${testomatLogoURL})](https://testomat.io) | ${statusEmoji(
82
- runParams.status,
83
- )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
82
+ runParams.status,
83
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
84
84
  | --- | --- |
85
85
  | Tests | ✔️ **${this.tests.length}** tests run |
86
86
  | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
87
- 'passed',
88
- )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
87
+ 'passed',
88
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
89
89
  | Duration | 🕐 **${humanizeDuration(
90
90
  parseInt(
91
91
  this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
@@ -42,6 +42,10 @@ class TestomatioPipe {
42
42
  return;
43
43
  }
44
44
  debug('Testomatio Pipe: Enabled');
45
+
46
+ const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
47
+ const proxy = proxyUrl ? new URL(proxyUrl) : null;
48
+
45
49
  this.parallel = params.parallel;
46
50
  this.store = store || {};
47
51
  this.title = params.title || process.env.TESTOMATIO_TITLE;
@@ -53,6 +57,11 @@ class TestomatioPipe {
53
57
  this.axios = axios.create({
54
58
  baseURL: `${this.url.trim()}`,
55
59
  timeout: AXIOS_TIMEOUT,
60
+ proxy: proxy ? {
61
+ host: proxy.hostname,
62
+ port: proxy.port,
63
+ protocol: proxy.protocol,
64
+ } : false,
56
65
  });
57
66
 
58
67
  // Pass the axios instance to the retry function
@@ -341,7 +350,7 @@ class TestomatioPipe {
341
350
  if (!this.runId) return;
342
351
 
343
352
  // add test ID + run ID
344
- data.rid = `${this.runId}-${data.rid}`;
353
+ if (data.rid) data.rid = `${this.runId}-${data.rid}`;
345
354
  data.api_key = this.apiKey;
346
355
  data.create = this.createNewTests;
347
356
 
@@ -106,6 +106,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
106
106
  };
107
107
 
108
108
  const TEST_ID_REGEX = /@T([\w\d]{8})/;
109
+ const SUITE_ID_REGEX = /@[Ss]([\w\d]{8})/;
109
110
 
110
111
  const fetchIdFromCode = (code, opts = {}) => {
111
112
  const comments = code
@@ -133,6 +134,10 @@ const fetchIdFromOutput = output => {
133
134
  return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
134
135
  };
135
136
 
137
+ const fetchSuiteId = (title) => {
138
+ return title.find(c => c.match(/@s/))?.match(TEST_ID_REGEX)?.[1];
139
+ };
140
+
136
141
  const fetchSourceCode = (contents, opts = {}) => {
137
142
  if (!opts.title && !opts.line) return '';
138
143
 
@@ -248,7 +253,7 @@ const foundedTestLog = (app, tests) => {
248
253
  const humanize = text => {
249
254
  // if there are no spaces, decamelize
250
255
  if (!text.trim().includes(' ')) text = decamelize(text);
251
-
256
+
252
257
  return text
253
258
  .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
254
259
  .trim()
package/lib/xmlReader.js CHANGED
@@ -528,4 +528,4 @@ function fetchProperties(item) {
528
528
  .filter(p => p.name === 'Category')
529
529
  .forEach(p => tags.push(p.value));
530
530
  return { title, tags };
531
- }
531
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -55,6 +55,7 @@
55
55
  "test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",
56
56
  "test:adapter:codecept:example": "codeceptjs run --config='./tests/adapter/examples/codecept/codecept.conf.js'",
57
57
  "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js",
58
+ "test:adapter:vitest:example": "vitest --config='./tests/adapter/examples/vitest/vitest.config.js'",
58
59
  "test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/logger-2.test.js && npx mocha tests-storage/reporter-functions.test.js"
59
60
  },
60
61
  "devDependencies": {