@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.
- package/lib/adapter/vitest.js +182 -0
- package/lib/bin/reportXml.js +9 -4
- package/lib/client.js +23 -8
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/testomatio.js +10 -1
- package/lib/utils/utils.js +6 -1
- package/lib/xmlReader.js +1 -1
- package/package.json +2 -1
|
@@ -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;
|
package/lib/bin/reportXml.js
CHANGED
|
@@ -44,10 +44,15 @@ program
|
|
|
44
44
|
|
|
45
45
|
let timeoutTimer;
|
|
46
46
|
if (opts.timelimit) {
|
|
47
|
-
timeoutTimer = setTimeout(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
292
|
-
|
|
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;
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -79,13 +79,13 @@ class GitLabPipe {
|
|
|
79
79
|
let summary = `${this.hiddenCommentData}
|
|
80
80
|
|
|
81
81
|
| [](https://testomat.io) | ${statusEmoji(
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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),
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -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
|
|
package/lib/utils/utils.js
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "1.4.
|
|
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": {
|