@testomatio/reporter 1.6.0-beta-2-artifacts → 2.0.0-beta-esm
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/codecept.js +288 -330
- package/lib/adapter/cucumber/current.js +195 -203
- package/lib/adapter/cucumber/legacy.js +130 -155
- package/lib/adapter/cucumber.js +5 -16
- package/lib/adapter/cypress-plugin/index.js +91 -105
- package/lib/adapter/jasmine/jasmine.js +63 -0
- package/lib/adapter/jasmine.js +54 -53
- package/lib/adapter/jest.js +97 -99
- package/lib/adapter/mocha/mocha.js +125 -0
- package/lib/adapter/mocha.js +111 -140
- package/lib/adapter/playwright.js +168 -200
- package/lib/adapter/vitest.js +144 -143
- package/lib/adapter/webdriver.js +113 -97
- package/lib/bin/reportXml.js +49 -49
- package/lib/bin/startTest.js +80 -97
- package/lib/client.js +344 -385
- package/lib/config.js +16 -21
- package/lib/constants.js +49 -43
- package/lib/data-storage.js +206 -188
- package/lib/fileUploader.js +245 -0
- package/lib/junit-adapter/adapter.js +17 -20
- package/lib/junit-adapter/csharp.js +18 -14
- package/lib/junit-adapter/index.js +27 -25
- package/lib/junit-adapter/java.js +41 -53
- package/lib/junit-adapter/javascript.js +30 -27
- package/lib/junit-adapter/python.js +38 -37
- package/lib/junit-adapter/ruby.js +11 -8
- package/lib/output.js +44 -52
- package/lib/package.json +1 -0
- package/lib/pipe/bitbucket.js +208 -227
- package/lib/pipe/csv.js +111 -124
- package/lib/pipe/github.js +184 -211
- package/lib/pipe/gitlab.js +164 -205
- package/lib/pipe/html.js +253 -312
- package/lib/pipe/index.js +83 -63
- package/lib/pipe/testomatio.js +391 -454
- package/lib/reporter-functions.js +16 -20
- package/lib/reporter.js +47 -17
- package/lib/services/artifacts.js +55 -51
- package/lib/services/index.js +14 -12
- package/lib/services/key-values.js +56 -53
- package/lib/services/logger.js +227 -245
- package/lib/utils/chalk.js +10 -0
- package/lib/utils/pipe_utils.js +91 -84
- package/lib/utils/utils.js +289 -273
- package/lib/xmlReader.js +480 -519
- package/package.json +57 -19
- package/src/adapter/codecept.js +369 -0
- package/src/adapter/cucumber/current.js +228 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +107 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +156 -0
- package/src/adapter/playwright.js +222 -0
- package/src/adapter/vitest.js +183 -0
- package/src/adapter/webdriver.js +111 -0
- package/src/bin/reportXml.js +67 -0
- package/src/bin/startTest.js +119 -0
- package/src/client.js +423 -0
- package/src/config.js +30 -0
- package/src/constants.js +49 -0
- package/src/data-storage.js +204 -0
- package/src/fileUploader.js +307 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +16 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +254 -0
- package/src/pipe/csv.js +140 -0
- package/src/pipe/github.js +234 -0
- package/src/pipe/gitlab.js +229 -0
- package/src/pipe/html.js +366 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/testomatio.js +498 -0
- package/src/reporter-functions.js +44 -0
- package/src/reporter.cjs +22 -0
- package/src/reporter.js +24 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +13 -0
- package/src/services/key-values.js +59 -0
- package/src/services/logger.js +314 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio.hbs +1421 -0
- package/src/utils/chalk.js +13 -0
- package/src/utils/pipe_utils.js +127 -0
- package/src/utils/utils.js +341 -0
- package/src/xmlReader.js +551 -0
- package/lib/bin/cli.js +0 -216
- package/lib/bin/uploadArtifacts.js +0 -86
- package/lib/uploader.js +0 -312
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
2
|
+
import Mocha from 'mocha';
|
|
3
|
+
import TestomatClient from '../client.js';
|
|
4
|
+
import { STATUS, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
5
|
+
import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
6
|
+
import { config } from '../config.js';
|
|
7
|
+
import { services } from '../services/index.js';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
EVENT_RUN_BEGIN,
|
|
12
|
+
EVENT_RUN_END,
|
|
13
|
+
EVENT_TEST_FAIL,
|
|
14
|
+
EVENT_TEST_PASS,
|
|
15
|
+
EVENT_TEST_PENDING,
|
|
16
|
+
EVENT_SUITE_BEGIN,
|
|
17
|
+
EVENT_SUITE_END,
|
|
18
|
+
EVENT_TEST_BEGIN,
|
|
19
|
+
EVENT_TEST_END,
|
|
20
|
+
} = Mocha.Runner.constants;
|
|
21
|
+
|
|
22
|
+
function MochaReporter(runner, opts) {
|
|
23
|
+
Mocha.reporters.Base.call(this, runner);
|
|
24
|
+
let passes = 0;
|
|
25
|
+
let failures = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
// let artifactStore;
|
|
28
|
+
|
|
29
|
+
const apiKey = opts?.reporterOptions?.apiKey || config.TESTOMATIO;
|
|
30
|
+
|
|
31
|
+
const client = new TestomatClient({ apiKey });
|
|
32
|
+
|
|
33
|
+
runner.on(EVENT_RUN_BEGIN, () => {
|
|
34
|
+
client.createRun();
|
|
35
|
+
|
|
36
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
runner.on(EVENT_SUITE_BEGIN, async suite => {
|
|
40
|
+
services.setContext(suite.fullTitle());
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
runner.on(EVENT_SUITE_END, async () => {
|
|
44
|
+
services.setContext(null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
runner.on(EVENT_TEST_BEGIN, async test => {
|
|
48
|
+
services.setContext(test.fullTitle());
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
runner.on(EVENT_TEST_END, async () => {
|
|
52
|
+
services.setContext(null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
runner.on(EVENT_TEST_PASS, async test => {
|
|
56
|
+
passes += 1;
|
|
57
|
+
|
|
58
|
+
console.log(pc.bold(pc.green('✔')), test.fullTitle());
|
|
59
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
60
|
+
|
|
61
|
+
const logs = getTestLogs(test);
|
|
62
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
63
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
64
|
+
|
|
65
|
+
client.addTestRun(STATUS.PASSED, {
|
|
66
|
+
test_id: testId,
|
|
67
|
+
suite_title: getSuiteTitle(test),
|
|
68
|
+
title: getTestName(test),
|
|
69
|
+
code: test.body.toString(),
|
|
70
|
+
file: getFile(test),
|
|
71
|
+
time: test.duration,
|
|
72
|
+
logs,
|
|
73
|
+
manuallyAttachedArtifacts: artifacts,
|
|
74
|
+
meta: keyValues,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
runner.on(EVENT_TEST_PENDING, test => {
|
|
79
|
+
skipped += 1;
|
|
80
|
+
console.log('skip: %s', test.fullTitle());
|
|
81
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
82
|
+
client.addTestRun(STATUS.SKIPPED, {
|
|
83
|
+
title: getTestName(test),
|
|
84
|
+
suite_title: getSuiteTitle(test),
|
|
85
|
+
code: test.body.toString(),
|
|
86
|
+
file: getFile(test),
|
|
87
|
+
test_id: testId,
|
|
88
|
+
time: test.duration,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
runner.on(EVENT_TEST_FAIL, async (test, err) => {
|
|
93
|
+
failures += 1;
|
|
94
|
+
console.log(pc.bold(pc.red('✖')), test.fullTitle(), pc.gray(err.message));
|
|
95
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
96
|
+
|
|
97
|
+
const logs = getTestLogs(test);
|
|
98
|
+
|
|
99
|
+
client.addTestRun(STATUS.FAILED, {
|
|
100
|
+
error: err,
|
|
101
|
+
suite_title: getSuiteTitle(test),
|
|
102
|
+
file: getFile(test),
|
|
103
|
+
test_id: testId,
|
|
104
|
+
title: getTestName(test),
|
|
105
|
+
code: test.body.toString(),
|
|
106
|
+
time: test.duration,
|
|
107
|
+
logs,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
runner.on(EVENT_RUN_END, () => {
|
|
112
|
+
const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
113
|
+
console.log(pc.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
client.updateRunStatus(status);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getTestLogs(test) {
|
|
120
|
+
const suiteLogsArr = services.logger.getLogs(test.parent.fullTitle());
|
|
121
|
+
const suiteLogs = suiteLogsArr ? suiteLogsArr.join('\n').trim() : '';
|
|
122
|
+
const testLogsArr = services.logger.getLogs(test.fullTitle());
|
|
123
|
+
const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
|
|
124
|
+
|
|
125
|
+
let logs = '';
|
|
126
|
+
if (suiteLogs) {
|
|
127
|
+
logs += `${pc.bold('\t--- BeforeSuite ---')}\n${suiteLogs}`;
|
|
128
|
+
}
|
|
129
|
+
if (testLogs) {
|
|
130
|
+
logs += `\n${pc.bold('\t--- Test ---')}\n${testLogs}`;
|
|
131
|
+
}
|
|
132
|
+
return logs;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getSuiteTitle(test, pathArr = []) {
|
|
136
|
+
if (test.parent.parent) getSuiteTitle(test.parent, pathArr);
|
|
137
|
+
|
|
138
|
+
pathArr.push(test.parent.title);
|
|
139
|
+
|
|
140
|
+
return pathArr.filter(t => !!t)[0];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getFile(test) {
|
|
144
|
+
return test.parent.file?.replace(process.cwd(), '');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getTestName(test) {
|
|
148
|
+
if (process.env.TESTOMATIO_CREATE === 'fulltitle') return test.fullTitle();
|
|
149
|
+
return test.title;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// To have this reporter "extend" a built-in reporter uncomment the following line:
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
Mocha.utils.inherits(MochaReporter, Mocha.reporters.Spec);
|
|
155
|
+
|
|
156
|
+
export default MochaReporter;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { APP_PREFIX, STATUS as Status, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
8
|
+
import TestomatioClient from '../client.js';
|
|
9
|
+
import { upload } from '../fileUploader.js';
|
|
10
|
+
import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
11
|
+
import { services } from '../services/index.js';
|
|
12
|
+
import { dataStorage } from '../data-storage.js';
|
|
13
|
+
|
|
14
|
+
const reportTestPromises = [];
|
|
15
|
+
|
|
16
|
+
class PlaywrightReporter {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.client = new TestomatioClient({ apiKey: config?.apiKey });
|
|
19
|
+
|
|
20
|
+
this.uploads = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onBegin(config, suite) {
|
|
24
|
+
// clean data storage
|
|
25
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
26
|
+
if (!this.client) return;
|
|
27
|
+
this.suite = suite;
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.client.createRun();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onTestBegin(testInfo) {
|
|
33
|
+
const fullTestTitle = getTestContextName(testInfo);
|
|
34
|
+
dataStorage.setContext(fullTestTitle);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onTestEnd(test, result) {
|
|
38
|
+
// test.parent.project().__projectId
|
|
39
|
+
|
|
40
|
+
if (!this.client) return;
|
|
41
|
+
|
|
42
|
+
const { title } = test;
|
|
43
|
+
const { error, duration } = result;
|
|
44
|
+
const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
|
|
45
|
+
|
|
46
|
+
const steps = [];
|
|
47
|
+
for (const step of result.steps) {
|
|
48
|
+
const appendedStep = appendStep(step);
|
|
49
|
+
if (appendedStep) {
|
|
50
|
+
steps.push(appendedStep);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fullTestTitle = getTestContextName(test);
|
|
55
|
+
let logs = '';
|
|
56
|
+
if (result.stderr.length || result.stdout.length) {
|
|
57
|
+
logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
|
|
58
|
+
}
|
|
59
|
+
const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
|
|
60
|
+
const keyValues = services.keyValues.get(fullTestTitle);
|
|
61
|
+
const rid = test.id || test.testId || uuidv4();
|
|
62
|
+
|
|
63
|
+
const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
|
|
64
|
+
rid,
|
|
65
|
+
error,
|
|
66
|
+
test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
|
|
67
|
+
suite_title,
|
|
68
|
+
title,
|
|
69
|
+
steps: steps.length ? steps : undefined,
|
|
70
|
+
time: duration,
|
|
71
|
+
logs,
|
|
72
|
+
manuallyAttachedArtifacts,
|
|
73
|
+
meta: keyValues,
|
|
74
|
+
file: test.location?.file,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.uploads.push({
|
|
78
|
+
rid,
|
|
79
|
+
title: test.title,
|
|
80
|
+
files: result.attachments.filter(a => a.body || a.path),
|
|
81
|
+
file: test.location?.file,
|
|
82
|
+
});
|
|
83
|
+
// remove empty uploads
|
|
84
|
+
this.uploads = this.uploads.filter(anUpload => anUpload.files.length);
|
|
85
|
+
|
|
86
|
+
reportTestPromises.push(reportTestPromise);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#getArtifactPath(artifact) {
|
|
90
|
+
if (artifact.path) {
|
|
91
|
+
if (path.isAbsolute(artifact.path)) return artifact.path;
|
|
92
|
+
|
|
93
|
+
return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (artifact.body) {
|
|
97
|
+
const fileName = tmpFile();
|
|
98
|
+
fs.writeFileSync(fileName, artifact.body);
|
|
99
|
+
return fileName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async onEnd(result) {
|
|
106
|
+
if (!this.client) return;
|
|
107
|
+
|
|
108
|
+
await Promise.all(reportTestPromises);
|
|
109
|
+
|
|
110
|
+
if (this.uploads.length && upload.isArtifactsEnabled()) {
|
|
111
|
+
console.log(APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
|
|
112
|
+
|
|
113
|
+
const promises = [];
|
|
114
|
+
|
|
115
|
+
for (const anUpload of this.uploads) {
|
|
116
|
+
const { rid, file, title } = anUpload;
|
|
117
|
+
|
|
118
|
+
const files = anUpload.files.map(attachment => ({
|
|
119
|
+
path: this.#getArtifactPath(attachment),
|
|
120
|
+
title,
|
|
121
|
+
type: attachment.contentType,
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
promises.push(
|
|
125
|
+
this.client.addTestRun(undefined, {
|
|
126
|
+
rid,
|
|
127
|
+
title,
|
|
128
|
+
files,
|
|
129
|
+
file,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
await Promise.all(promises);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.client.updateRunStatus(checkStatus(result.status));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkStatus(status) {
|
|
141
|
+
return (
|
|
142
|
+
{
|
|
143
|
+
skipped: Status.SKIPPED,
|
|
144
|
+
timedOut: Status.FAILED,
|
|
145
|
+
passed: Status.PASSED,
|
|
146
|
+
}[status] || Status.FAILED
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function appendStep(step, shift = 0) {
|
|
151
|
+
// nesting too deep, ignore those steps
|
|
152
|
+
if (shift >= 10) return;
|
|
153
|
+
|
|
154
|
+
let newCategory = step.category;
|
|
155
|
+
switch (newCategory) {
|
|
156
|
+
case 'test.step':
|
|
157
|
+
newCategory = 'user';
|
|
158
|
+
break;
|
|
159
|
+
case 'hook':
|
|
160
|
+
newCategory = 'hook';
|
|
161
|
+
break;
|
|
162
|
+
case 'attach':
|
|
163
|
+
return null; // Skip steps with category 'attach'
|
|
164
|
+
default:
|
|
165
|
+
newCategory = 'framework';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const formattedSteps = [];
|
|
169
|
+
for (const child of step.steps || []) {
|
|
170
|
+
const appendedChild = appendStep(child, shift + 2);
|
|
171
|
+
if (appendedChild) {
|
|
172
|
+
formattedSteps.push(appendedChild);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const resultStep = {
|
|
177
|
+
category: newCategory,
|
|
178
|
+
title: step.title,
|
|
179
|
+
duration: step.duration,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (formattedSteps.length) {
|
|
183
|
+
resultStep.steps = formattedSteps.filter(s => !!s);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (step.error !== undefined) {
|
|
187
|
+
resultStep.error = step.error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return resultStep;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function tmpFile(prefix = 'tmp.') {
|
|
194
|
+
const tmpdir = os.tmpdir();
|
|
195
|
+
return path.join(tmpdir, prefix + crypto.randomBytes(16).toString('hex'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns filename + test title
|
|
200
|
+
* @param {*} test - testInfo object from Playwright
|
|
201
|
+
* @returns
|
|
202
|
+
*/
|
|
203
|
+
function getTestContextName(test) {
|
|
204
|
+
return `${test._requireFile || ''}_${test.title}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initPlaywrightForStorage() {
|
|
208
|
+
try {
|
|
209
|
+
// @ts-ignore-next-line
|
|
210
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
211
|
+
const { test } = require('@playwright/test');
|
|
212
|
+
// eslint-disable-next-line no-empty-pattern
|
|
213
|
+
test.beforeEach(async ({}, testInfo) => {
|
|
214
|
+
global.testomatioTestTitle = `${testInfo.file || ''}_${testInfo.title}`;
|
|
215
|
+
});
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default PlaywrightReporter;
|
|
222
|
+
export { initPlaywrightForStorage };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { Client as TestomatioClient } from '../client.js';
|
|
3
|
+
import { STATUS } from '../constants.js';
|
|
4
|
+
import { getTestomatIdFromTestTitle } from '../utils/utils.js';
|
|
5
|
+
import createDebugMessages from 'debug';
|
|
6
|
+
|
|
7
|
+
const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('../../types').VitestTest} VitestTest
|
|
11
|
+
* @typedef {import('../../types').VitestTestFile} VitestTestFile
|
|
12
|
+
* @typedef {import('../../types').VitestSuite} VitestSuite
|
|
13
|
+
* @typedef {import('../../types').VitestTestLogs} VitestTestLogs
|
|
14
|
+
* @typedef {import('../../vitest.types').ErrorWithDiff} ErrorWithDiff
|
|
15
|
+
* @typedef {typeof import('../constants.js').STATUS} STATUS
|
|
16
|
+
* @typedef {import('../../types').TestData} TestData
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
class VitestReporter {
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.client = new TestomatioClient({ apiKey: config?.apiKey });
|
|
22
|
+
/**
|
|
23
|
+
* @type {(TestData & {status: string})[]} tests
|
|
24
|
+
*/
|
|
25
|
+
this.tests = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// on run start
|
|
29
|
+
onInit() {
|
|
30
|
+
this.client.createRun();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
35
|
+
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
36
|
+
*/
|
|
37
|
+
async onFinished(files, errors) {
|
|
38
|
+
if (!files || !files.length) console.info('No tests executed');
|
|
39
|
+
|
|
40
|
+
files.forEach(file => {
|
|
41
|
+
// task could be test or suite
|
|
42
|
+
file.tasks.forEach(taskOrSuite => {
|
|
43
|
+
if (taskOrSuite.type === 'test') {
|
|
44
|
+
const test = taskOrSuite;
|
|
45
|
+
this.tests.push(this.#getDataFromTest(test));
|
|
46
|
+
} else if (taskOrSuite.type === 'suite') {
|
|
47
|
+
const suite = taskOrSuite;
|
|
48
|
+
this.#processTasksOfSuite(suite);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error('Unprocessed case. Unknown task type');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
debug(this.tests.length, 'tests collected');
|
|
56
|
+
|
|
57
|
+
// send tests to Testomat.io
|
|
58
|
+
for (const test of this.tests) {
|
|
59
|
+
await this.client.addTestRun(test.status, test);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('finished');
|
|
63
|
+
if (errors.length) console.error('Vitest adapter errors:', errors);
|
|
64
|
+
|
|
65
|
+
await this.client.updateRunStatus(getRunStatusFromResults(files));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* non-used listeners
|
|
69
|
+
onUserConsoleLog(log) {}
|
|
70
|
+
onPathsCollected(paths) {} // paths array to files with tests
|
|
71
|
+
onCollected(files) {} // files array with tests (but without results)
|
|
72
|
+
onTaskUpdate(packs) {} // some updates come here on afterAll block execution
|
|
73
|
+
onTestRemoved(trigger) {}
|
|
74
|
+
onWatcherStart(files, errors) {}
|
|
75
|
+
onWatcherRerun(files, trigger) {}
|
|
76
|
+
onServerRestart(reason) {}
|
|
77
|
+
onProcessTimeout() {}
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recursively gets all tasks from suite and pushes them to "tests" array
|
|
82
|
+
*
|
|
83
|
+
* @param {VitestSuite} suite
|
|
84
|
+
*/
|
|
85
|
+
#processTasksOfSuite(suite) {
|
|
86
|
+
suite.tasks.forEach(taskOrSuite => {
|
|
87
|
+
if (taskOrSuite.type === 'test') {
|
|
88
|
+
const test = taskOrSuite;
|
|
89
|
+
this.tests.push(this.#getDataFromTest(test));
|
|
90
|
+
} else if (taskOrSuite.type === 'suite') {
|
|
91
|
+
const theSuite = taskOrSuite;
|
|
92
|
+
this.#processTasksOfSuite(theSuite);
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error('Unprocessed case. Unknown task type');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Processes task and returns test data ready to be sent to Testomat.io
|
|
101
|
+
*
|
|
102
|
+
* @param {VitestTest} test
|
|
103
|
+
*
|
|
104
|
+
* @returns {TestData & {status: string}}
|
|
105
|
+
*/
|
|
106
|
+
#getDataFromTest(test) {
|
|
107
|
+
return {
|
|
108
|
+
error: test.result?.errors ? test.result.errors[0] : undefined,
|
|
109
|
+
file: test.file.name,
|
|
110
|
+
logs: test.logs ? transformLogsToString(test.logs) : '',
|
|
111
|
+
meta: test.meta,
|
|
112
|
+
status: getTestStatus(test),
|
|
113
|
+
suite_title: test.suite.name || test.file?.name,
|
|
114
|
+
test_id: getTestomatIdFromTestTitle(test.name),
|
|
115
|
+
time: test.result?.duration || 0,
|
|
116
|
+
title: test.name,
|
|
117
|
+
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns run status based on test results
|
|
124
|
+
*
|
|
125
|
+
* @param {VitestTestFile[]} files
|
|
126
|
+
* @returns {'passed' | 'failed' | 'finished'}
|
|
127
|
+
*/
|
|
128
|
+
function getRunStatusFromResults(files) {
|
|
129
|
+
/**
|
|
130
|
+
* @type {'passed' | 'failed' | 'finished'}
|
|
131
|
+
*/
|
|
132
|
+
let status = 'finished'; // default status (if no failed or passed tests)
|
|
133
|
+
|
|
134
|
+
files.forEach(file => {
|
|
135
|
+
// search for failed tests
|
|
136
|
+
file.tasks.forEach(taskOrSuite => {
|
|
137
|
+
if (taskOrSuite.result?.state === 'fail') {
|
|
138
|
+
status = 'failed'; // set status to failed if any test failed
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// if there are no failed tests > search for passed tests
|
|
143
|
+
if (status !== 'failed') {
|
|
144
|
+
file.tasks.forEach(taskOrSuite => {
|
|
145
|
+
if (taskOrSuite.result?.state === 'pass') {
|
|
146
|
+
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return status;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns test status in Testomat.io format
|
|
157
|
+
*
|
|
158
|
+
* @param {VitestTest} test
|
|
159
|
+
* @returns 'passed' | 'failed' | 'skipped'
|
|
160
|
+
*/
|
|
161
|
+
function getTestStatus(test) {
|
|
162
|
+
if (test.result?.state === 'fail') return STATUS.FAILED;
|
|
163
|
+
if (test.result?.state === 'pass') return STATUS.PASSED;
|
|
164
|
+
if (!test.result && test.mode === 'skip') return STATUS.SKIPPED;
|
|
165
|
+
console.error(pc.red('Unprocessed case for defining test status. Contact dev team. Test:'), test);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {VitestTestLogs[]} logs
|
|
170
|
+
* @returns string
|
|
171
|
+
*/
|
|
172
|
+
function transformLogsToString(logs) {
|
|
173
|
+
if (!logs) return '';
|
|
174
|
+
let logsStr = '';
|
|
175
|
+
logs.forEach(log => {
|
|
176
|
+
if (log.type === 'stdout') logsStr += `${log.content}\n`;
|
|
177
|
+
if (log.type === 'stderr') logsStr += `${pc.red(log.content)}\n`;
|
|
178
|
+
});
|
|
179
|
+
return logsStr;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default VitestReporter;
|
|
183
|
+
export { VitestReporter };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// eslint-disable-next-line
|
|
2
|
+
import WDIOReporter, { RunnerStats } from '@wdio/reporter';
|
|
3
|
+
|
|
4
|
+
import TestomatClient from '../client.js';
|
|
5
|
+
import { getTestomatIdFromTestTitle } from '../utils/utils.js';
|
|
6
|
+
|
|
7
|
+
class WebdriverReporter extends WDIOReporter {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super(options);
|
|
10
|
+
|
|
11
|
+
this.client = new TestomatClient({ apiKey: options?.apiKey });
|
|
12
|
+
options = Object.assign(options, { stdout: true });
|
|
13
|
+
|
|
14
|
+
this._addTestPromises = [];
|
|
15
|
+
|
|
16
|
+
this._isSynchronising = false;
|
|
17
|
+
// NOTE: new functionality; may break everything
|
|
18
|
+
this.client.createRun();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get isSynchronised() {
|
|
22
|
+
return this._isSynchronising === false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param {RunnerStats} runData
|
|
28
|
+
*/
|
|
29
|
+
async onRunnerEnd(runData) {
|
|
30
|
+
this._isSynchronising = true;
|
|
31
|
+
|
|
32
|
+
await Promise.all(this._addTestPromises);
|
|
33
|
+
|
|
34
|
+
this._isSynchronising = false;
|
|
35
|
+
|
|
36
|
+
// NOTE: new functionality; may break everything
|
|
37
|
+
// also this may require additional status mapping
|
|
38
|
+
await this.client.updateRunStatus(runData.failures ? 'failed' : 'passed');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onTestEnd(test) {
|
|
42
|
+
this._addTestPromises.push(this.addTest(test));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// wdio-cucumber does not trigger onTestEnd hook, thus, using this one
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
onSuiteEnd(scerario) {
|
|
51
|
+
if (scerario.type === 'scenario') {
|
|
52
|
+
this._addTestPromises.push(this.addBddScenario(scerario));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async addTest(test) {
|
|
57
|
+
if (!this.client) return;
|
|
58
|
+
|
|
59
|
+
const { title, _duration: duration, state, error, output } = test;
|
|
60
|
+
|
|
61
|
+
const testId = getTestomatIdFromTestTitle(title);
|
|
62
|
+
|
|
63
|
+
const screenshotEndpoint = '/session/:sessionId/screenshot';
|
|
64
|
+
const screenshotsBuffers = output
|
|
65
|
+
.filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
|
|
66
|
+
.map(el => Buffer.from(el.result.value, 'base64'));
|
|
67
|
+
|
|
68
|
+
await this.client.addTestRun(state, {
|
|
69
|
+
error,
|
|
70
|
+
title,
|
|
71
|
+
test_id: testId,
|
|
72
|
+
time: duration,
|
|
73
|
+
filesBuffers: screenshotsBuffers,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {import('../../types').WebdriverIOScenario} scenario
|
|
79
|
+
*/
|
|
80
|
+
addBddScenario(scenario) {
|
|
81
|
+
if (!this.client) return;
|
|
82
|
+
|
|
83
|
+
const { title, _duration: duration } = scenario;
|
|
84
|
+
|
|
85
|
+
const testId = getTestomatIdFromTestTitle(title || scenario.tags.map(tag => tag.name).join(' '));
|
|
86
|
+
|
|
87
|
+
let scenarioState = scenario.tests.every(test => test.state === 'passed') ? 'passed' : 'failed';
|
|
88
|
+
if (scenario.tests.every(test => test.state === 'skipped')) {
|
|
89
|
+
scenarioState = 'skipped';
|
|
90
|
+
}
|
|
91
|
+
const errors = scenario.tests
|
|
92
|
+
.filter(test => test.state === 'failed')
|
|
93
|
+
.map(test => test.error?.stack)
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
const error = errors.join('\n');
|
|
96
|
+
|
|
97
|
+
const tags = scenario.tags.map(tag => tag.name);
|
|
98
|
+
|
|
99
|
+
return this.client.addTestRun(scenarioState, {
|
|
100
|
+
error: error ? Error(error) : null,
|
|
101
|
+
title,
|
|
102
|
+
test_id: testId,
|
|
103
|
+
time: duration,
|
|
104
|
+
tags,
|
|
105
|
+
file: scenario.file,
|
|
106
|
+
// filesBuffers: screenshotsBuffers,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default WebdriverReporter;
|