@testomatio/reporter 0.8.0-beta.8 → 0.8.1
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/README.md +77 -7
- package/lib/ArtifactStorage.js +131 -0
- package/lib/adapter/codecept.js +25 -24
- package/lib/adapter/cucumber/current.js +16 -12
- package/lib/adapter/cucumber/legacy.js +14 -10
- package/lib/adapter/cypress-plugin/index.js +7 -7
- package/lib/adapter/jasmine.js +5 -4
- package/lib/adapter/jest.js +7 -11
- package/lib/adapter/mocha.js +69 -18
- package/lib/adapter/playwright.js +8 -5
- package/lib/adapter/webdriver.js +2 -1
- package/lib/bin/reportXml.js +5 -2
- package/lib/bin/startTest.js +2 -2
- package/lib/client.js +73 -49
- package/lib/constants.js +17 -2
- package/lib/fileUploader.js +86 -49
- package/lib/junit-adapter/csharp.js +0 -1
- package/lib/junit-adapter/index.js +3 -2
- package/lib/pipe/csv.js +133 -0
- package/lib/pipe/github.js +137 -73
- package/lib/pipe/gitlab.js +229 -0
- package/lib/pipe/index.js +27 -12
- package/lib/pipe/testomatio.js +71 -44
- package/lib/reporter.js +2 -0
- package/lib/util.js +46 -13
- package/lib/xmlReader.js +47 -31
- package/package.json +12 -3
- package/testcafe/index.js +0 -61
- package/testcafe/package.json +0 -14
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:gitlab');
|
|
2
|
+
const { default: axios } = require('axios');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const humanizeDuration = require('humanize-duration');
|
|
5
|
+
const merge = require('lodash.merge');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { APP_PREFIX } = require('../constants');
|
|
8
|
+
const { ansiRegExp, isSameTest } = require('../util');
|
|
9
|
+
|
|
10
|
+
//! GITLAB_PAT environment variable is required for this functionality to work
|
|
11
|
+
//! and your pipeline trigger should be merge_request
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @class GitLabPipe
|
|
15
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
16
|
+
* @typedef {import('../../types').TestData} TestData
|
|
17
|
+
*/
|
|
18
|
+
class GitLabPipe {
|
|
19
|
+
constructor(params, store = {}) {
|
|
20
|
+
this.isEnabled = false;
|
|
21
|
+
this.ENV = process.env;
|
|
22
|
+
this.store = store;
|
|
23
|
+
this.tests = [];
|
|
24
|
+
// GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
|
|
25
|
+
this.token = params.GITLAB_PAT || this.ENV.GITLAB_PAT;
|
|
26
|
+
this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
|
|
27
|
+
|
|
28
|
+
debug(
|
|
29
|
+
chalk.yellow('GitLab Pipe:'),
|
|
30
|
+
this.token ? 'TOKEN passed' : '*no token*',
|
|
31
|
+
`Project id: ${this.ENV.CI_PROJECT_ID}, MR id: ${this.ENV.CI_MERGE_REQUEST_IID}`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!this.ENV.CI_PROJECT_ID || !this.ENV.CI_MERGE_REQUEST_IID) {
|
|
35
|
+
debug(`CI pipeline should be run in Merge Request to have ability to add the report comment.`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!this.token) {
|
|
40
|
+
debug(`Hint: GitLab CI variables are unavailable for unprotected branches by default.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.isEnabled = true;
|
|
45
|
+
|
|
46
|
+
debug('GitLab Pipe: Enabled');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async createRun() {}
|
|
50
|
+
|
|
51
|
+
addTest(test) {
|
|
52
|
+
if (!this.isEnabled) return;
|
|
53
|
+
|
|
54
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
55
|
+
// update if they were already added
|
|
56
|
+
if (index >= 0) {
|
|
57
|
+
this.tests[index] = merge(this.tests[index], test);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.tests.push(test);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async finishRun(runParams) {
|
|
65
|
+
if (!this.isEnabled) return;
|
|
66
|
+
|
|
67
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
68
|
+
|
|
69
|
+
// ... create a comment on GitLab
|
|
70
|
+
const passedCount = this.tests.filter(t => t.status === 'passed').length;
|
|
71
|
+
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
72
|
+
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
73
|
+
|
|
74
|
+
// constructing the table
|
|
75
|
+
let summary = `${this.hiddenCommentData}
|
|
76
|
+
|
|
77
|
+
| [](https://testomat.io) | ${
|
|
78
|
+
statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
81
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
82
|
+
'passed',
|
|
83
|
+
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
84
|
+
| Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
|
|
88
|
+
// eslint-disable-next-line max-len
|
|
89
|
+
summary += `| Job | 👷 [${this.ENV.CI_JOB_ID}](${this.ENV.CI_JOB_URL})<br>Name: **${this.ENV.CI_JOB_NAME}**<br>Stage: **${this.ENV.CI_JOB_STAGE}** | `;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const failures = this.tests
|
|
93
|
+
.filter(t => t.status === 'failed')
|
|
94
|
+
.slice(0, 20)
|
|
95
|
+
.map(t => {
|
|
96
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
97
|
+
text += '\n\n';
|
|
98
|
+
if (t.message)
|
|
99
|
+
text += `> ${t.message
|
|
100
|
+
.replace(/[^\x20-\x7E]/g, '')
|
|
101
|
+
.replace(ansiRegExp(), '')
|
|
102
|
+
.trim()}\n`;
|
|
103
|
+
if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
|
|
104
|
+
|
|
105
|
+
if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
106
|
+
t.artifacts
|
|
107
|
+
.filter(f => !!f)
|
|
108
|
+
.filter(f => f.endsWith('.png'))
|
|
109
|
+
.forEach(f => {
|
|
110
|
+
if (f.endsWith('.png')) {
|
|
111
|
+
text += `\n`;
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
text += `[📄 ${path.basename(f)}](${f})\n`;
|
|
115
|
+
return text;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
text += '\n---\n';
|
|
120
|
+
|
|
121
|
+
return text;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let body = summary;
|
|
125
|
+
|
|
126
|
+
if (failures.length) {
|
|
127
|
+
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
128
|
+
if (failures.length > 20) {
|
|
129
|
+
body += '\n> Notice\n> Only first 20 failures shown*';
|
|
130
|
+
}
|
|
131
|
+
body += '\n\n</details>';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.tests.length > 0) {
|
|
135
|
+
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
136
|
+
body += this.tests
|
|
137
|
+
// eslint-disable-next-line no-unsafe-optional-chaining
|
|
138
|
+
.sort((a, b) => b?.run_time - a?.run_time)
|
|
139
|
+
.slice(0, 5)
|
|
140
|
+
.map(t => `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`)
|
|
141
|
+
.join('\n');
|
|
142
|
+
body += '\n</details>';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// eslint-disable-next-line max-len
|
|
146
|
+
const commentsRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
|
|
147
|
+
|
|
148
|
+
// delete previous report
|
|
149
|
+
await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
|
|
150
|
+
|
|
151
|
+
// add current report
|
|
152
|
+
debug(`Adding comment via url: ${commentsRequestURL}`);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const addCommentResponse = await axios.post(`${commentsRequestURL}?access_token=${this.token}`, { body });
|
|
156
|
+
|
|
157
|
+
const commentID = addCommentResponse.data.id;
|
|
158
|
+
// eslint-disable-next-line max-len
|
|
159
|
+
const commentURL = `${this.ENV.CI_PROJECT_URL}/-/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}#note_${commentID}`;
|
|
160
|
+
|
|
161
|
+
console.log(APP_PREFIX, chalk.yellow('GitLab'), `Report created: ${chalk.magenta(commentURL)}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(
|
|
164
|
+
APP_PREFIX,
|
|
165
|
+
chalk.yellow('GitLab'),
|
|
166
|
+
`Couldn't create GitLab report\n${err}.
|
|
167
|
+
Request url: ${commentsRequestURL}
|
|
168
|
+
Request data: ${body}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
toString() {
|
|
174
|
+
return 'GitLab Reporter';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
updateRun() {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function statusEmoji(status) {
|
|
181
|
+
if (status === 'passed') return '🟢';
|
|
182
|
+
if (status === 'failed') return '🔴';
|
|
183
|
+
if (status === 'skipped') return '🟡';
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function fullName(t) {
|
|
188
|
+
let line = '';
|
|
189
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
190
|
+
line += `**${t.title}**`;
|
|
191
|
+
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
192
|
+
return line;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
196
|
+
if (process.env.GITLAB_KEEP_OUTDATED_REPORTS) return;
|
|
197
|
+
|
|
198
|
+
// get comments
|
|
199
|
+
let comments = [];
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const response = await axiosInstance.get(`${commentsRequestURL}?access_token=${token}`);
|
|
203
|
+
comments = response.data;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error('Error while attempt to retrieve comments on GitLab Merge Request:\n', e);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!comments.length) return;
|
|
209
|
+
|
|
210
|
+
for (const comment of comments) {
|
|
211
|
+
// if comment was left by the same workflow
|
|
212
|
+
if (comment.body.includes(hiddenCommentData)) {
|
|
213
|
+
try {
|
|
214
|
+
// delete previous comment
|
|
215
|
+
const deleteCommentURL = `${commentsRequestURL}/${comment.id}?access_token=${token}`;
|
|
216
|
+
await axiosInstance.delete(deleteCommentURL);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// pass next env var if need to clear all previous reports;
|
|
222
|
+
// only the last one is removed by default
|
|
223
|
+
if (!process.env.GITLAB_REMOVE_ALL_OUTDATED_REPORTS) break;
|
|
224
|
+
// TODO: in case of many reports should implement pagination
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = GitLabPipe;
|
package/lib/pipe/index.js
CHANGED
|
@@ -1,48 +1,63 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
3
4
|
const { APP_PREFIX } = require('../constants');
|
|
4
5
|
const TestomatioPipe = require('./testomatio');
|
|
5
6
|
const GitHubPipe = require('./github');
|
|
7
|
+
const GitLabPipe = require('./gitlab');
|
|
8
|
+
const CsvPipe = require('./csv');
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
function PipeFactory(params, opts) {
|
|
8
11
|
const extraPipes = [];
|
|
9
12
|
|
|
10
13
|
// Add extra pipes into package.json file:
|
|
11
14
|
// "testomatio": {
|
|
12
|
-
// "pipes": ["my-module-pipe", "./local/js/file/pipe"]
|
|
15
|
+
// "pipes": ["my-module-pipe", "./local/js/file/pipe"]
|
|
13
16
|
// }
|
|
14
17
|
|
|
15
18
|
const packageJsonFile = path.join(process.cwd(), 'package.json');
|
|
16
19
|
if (fs.existsSync(packageJsonFile)) {
|
|
17
|
-
const
|
|
18
|
-
const pipeDefs =
|
|
19
|
-
|
|
20
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonFile).toString());
|
|
21
|
+
const pipeDefs = packageJson?.testomatio?.pipes || [];
|
|
22
|
+
|
|
20
23
|
for (const pipeDef of pipeDefs) {
|
|
21
|
-
let
|
|
24
|
+
let PipeClass;
|
|
22
25
|
try {
|
|
23
|
-
|
|
26
|
+
PipeClass = require(pipeDef);
|
|
24
27
|
} catch (err) {
|
|
25
28
|
console.log(APP_PREFIX, `Can't load module Testomatio pipe module from ${pipeDef}`);
|
|
26
29
|
continue;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
try {
|
|
30
|
-
extraPipes.push(new
|
|
33
|
+
extraPipes.push(new PipeClass(params, opts));
|
|
31
34
|
} catch (err) {
|
|
32
35
|
console.log(APP_PREFIX, `Can't instantiate Testomatio for ${pipeDef}`, err);
|
|
33
36
|
continue;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
|
|
38
40
|
|
|
39
41
|
const pipes = [
|
|
40
42
|
new TestomatioPipe(params, opts),
|
|
41
|
-
new GitHubPipe(params, opts),
|
|
42
|
-
|
|
43
|
+
new GitHubPipe(params, opts),
|
|
44
|
+
new GitLabPipe(params, opts),
|
|
45
|
+
new CsvPipe(params, opts),
|
|
46
|
+
...extraPipes,
|
|
43
47
|
];
|
|
44
48
|
|
|
45
|
-
console.log(
|
|
49
|
+
console.log(
|
|
50
|
+
APP_PREFIX,
|
|
51
|
+
chalk.cyan('Pipes:'),
|
|
52
|
+
chalk.cyan(
|
|
53
|
+
pipes
|
|
54
|
+
.filter(p => p.isEnabled)
|
|
55
|
+
.map(p => p.toString())
|
|
56
|
+
.join(', ') || 'No pipes enabled',
|
|
57
|
+
),
|
|
58
|
+
);
|
|
46
59
|
|
|
47
60
|
return pipes;
|
|
48
61
|
}
|
|
62
|
+
|
|
63
|
+
module.exports = PipeFactory;
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -1,100 +1,114 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:testomatio');
|
|
1
2
|
const chalk = require('chalk');
|
|
2
3
|
const axios = require('axios');
|
|
3
4
|
const JsonCycle = require('json-cycle');
|
|
4
|
-
const {
|
|
5
|
-
const { APP_PREFIX } = require('../constants');
|
|
5
|
+
const { APP_PREFIX, STATUS } = require('../constants');
|
|
6
6
|
const { isValidUrl } = require('../util');
|
|
7
7
|
|
|
8
|
+
const { TESTOMATIO_RUN } = process.env;
|
|
8
9
|
if (TESTOMATIO_RUN) {
|
|
9
10
|
process.env.runId = TESTOMATIO_RUN;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
15
|
+
* @typedef {import('../../types').TestData} TestData
|
|
16
|
+
* @class TestomatioPipe
|
|
17
|
+
* @implements {Pipe}
|
|
18
|
+
*/
|
|
12
19
|
class TestomatioPipe {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
constructor(params, store = {}) {
|
|
20
|
+
constructor(params, store) {
|
|
21
|
+
this.isEnabled = false;
|
|
16
22
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
17
23
|
this.apiKey = params.apiKey || process.env.TESTOMATIO;
|
|
18
|
-
|
|
19
|
-
console.log(APP_PREFIX, 'Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
20
|
-
}
|
|
24
|
+
debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
21
25
|
if (!this.apiKey) {
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
this.store = store;
|
|
28
|
+
debug('Testomatio Pipe: Enabled');
|
|
29
|
+
this.store = store || {};
|
|
28
30
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
31
|
+
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
32
|
+
this.env = process.env.TESTOMATIO_ENV;
|
|
29
33
|
this.axios = axios.create();
|
|
30
34
|
this.isEnabled = true;
|
|
35
|
+
// do not finish this run (for parallel testing)
|
|
31
36
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
32
37
|
this.runId = params.runId || process.env.runId;
|
|
33
38
|
this.createNewTests = !!process.env.TESTOMATIO_CREATE;
|
|
34
39
|
|
|
35
40
|
if (!isValidUrl(this.url.trim())) {
|
|
36
41
|
this.isEnabled = false;
|
|
37
|
-
console.
|
|
38
|
-
APP_PREFIX,
|
|
39
|
-
chalk.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`),
|
|
40
|
-
);
|
|
41
|
-
return;
|
|
42
|
+
console.error(APP_PREFIX, chalk.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`));
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* @returns Promise<void>
|
|
48
|
+
*/
|
|
49
|
+
async createRun() {
|
|
46
50
|
if (!this.isEnabled) return;
|
|
47
51
|
|
|
48
|
-
runParams
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const runParams = Object.fromEntries(
|
|
53
|
+
Object.entries({
|
|
54
|
+
api_key: this.apiKey.trim(),
|
|
55
|
+
group_title: this.groupTitle,
|
|
56
|
+
env: this.env,
|
|
57
|
+
title: this.title,
|
|
58
|
+
}).filter(([, value]) => !!value)
|
|
59
|
+
);
|
|
60
|
+
|
|
51
61
|
if (this.runId) {
|
|
52
|
-
return this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams)
|
|
62
|
+
return this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams);
|
|
53
63
|
}
|
|
54
|
-
|
|
64
|
+
|
|
55
65
|
try {
|
|
56
66
|
const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
|
|
57
67
|
maxContentLength: Infinity,
|
|
58
68
|
maxBodyLength: Infinity,
|
|
59
|
-
})
|
|
69
|
+
});
|
|
60
70
|
this.runId = resp.data.uid;
|
|
61
71
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
62
72
|
this.store.runUrl = this.runUrl;
|
|
63
|
-
this.store.runId = this.runId;
|
|
73
|
+
this.store.runId = this.runId;
|
|
64
74
|
console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
|
|
65
75
|
process.env.runId = this.runId;
|
|
66
76
|
} catch (err) {
|
|
67
|
-
console.
|
|
77
|
+
console.error(
|
|
68
78
|
APP_PREFIX,
|
|
69
79
|
'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
|
|
70
80
|
);
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
/**
|
|
85
|
+
*
|
|
86
|
+
* @param testData data
|
|
87
|
+
* @returns
|
|
88
|
+
*/
|
|
89
|
+
addTest(data) {
|
|
75
90
|
if (!this.isEnabled) return;
|
|
76
91
|
if (!this.runId) return;
|
|
77
92
|
data.api_key = this.apiKey;
|
|
78
93
|
data.create = this.createNewTests;
|
|
79
94
|
const json = JsonCycle.stringify(data);
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} catch (err) {
|
|
96
|
+
return this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
|
|
97
|
+
maxContentLength: Infinity,
|
|
98
|
+
maxBodyLength: Infinity,
|
|
99
|
+
headers: {
|
|
100
|
+
// Overwrite Axios's automatically set Content-Type
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
.catch((err) => {
|
|
91
105
|
if (err.response) {
|
|
92
106
|
if (err.response.status >= 400) {
|
|
93
|
-
const
|
|
107
|
+
const responseData = err.response.data || { message: '' };
|
|
94
108
|
console.log(
|
|
95
109
|
APP_PREFIX,
|
|
96
110
|
chalk.blue(this.title),
|
|
97
|
-
`Report couldn't be processed: (${err.response.status}) ${
|
|
111
|
+
`Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
|
|
98
112
|
);
|
|
99
113
|
return;
|
|
100
114
|
}
|
|
@@ -102,18 +116,31 @@ class TestomatioPipe {
|
|
|
102
116
|
} else {
|
|
103
117
|
console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
|
|
104
118
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
119
|
+
});
|
|
107
120
|
}
|
|
108
121
|
|
|
122
|
+
/**
|
|
123
|
+
* @param {import('../../types').RunData} params
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
109
126
|
async finishRun(params) {
|
|
110
127
|
if (!this.isEnabled) return;
|
|
128
|
+
|
|
129
|
+
const { status, parallel } = params;
|
|
130
|
+
|
|
131
|
+
let status_event;
|
|
132
|
+
|
|
133
|
+
if (status === STATUS.FINISHED) status_event = 'finish';
|
|
134
|
+
if (status === STATUS.PASSED) status_event = 'pass';
|
|
135
|
+
if (status === STATUS.FAILED) status_event = 'fail';
|
|
136
|
+
if (parallel) status_event += '_parallel';
|
|
137
|
+
|
|
111
138
|
try {
|
|
112
139
|
if (this.runId && !this.proceed) {
|
|
113
140
|
await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
|
|
114
141
|
api_key: this.apiKey,
|
|
115
|
-
status_event
|
|
116
|
-
|
|
142
|
+
status_event,
|
|
143
|
+
tests: params.tests,
|
|
117
144
|
});
|
|
118
145
|
if (this.runUrl) {
|
|
119
146
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
|
|
@@ -134,4 +161,4 @@ class TestomatioPipe {
|
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
module.exports = TestomatioPipe;
|
|
164
|
+
module.exports = TestomatioPipe;
|
package/lib/reporter.js
CHANGED
package/lib/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { URL } = require('url');
|
|
2
|
-
const { sep } = require('path');
|
|
2
|
+
const { sep, basename } = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const isValid = require('is-valid-path');
|
|
@@ -7,7 +7,7 @@ const isValid = require('is-valid-path');
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {String} testTitle - Test title
|
|
9
9
|
*
|
|
10
|
-
* @returns {String} testId
|
|
10
|
+
* @returns {String|null} testId
|
|
11
11
|
*/
|
|
12
12
|
const parseTest = testTitle => {
|
|
13
13
|
const captures = testTitle.match(/@T([\w\d]+)/);
|
|
@@ -21,7 +21,7 @@ const parseTest = testTitle => {
|
|
|
21
21
|
/**
|
|
22
22
|
* @param {String} suiteTitle - suite title
|
|
23
23
|
*
|
|
24
|
-
* @returns {String} suiteId
|
|
24
|
+
* @returns {String|null} suiteId
|
|
25
25
|
*/
|
|
26
26
|
const parseSuite = suiteTitle => {
|
|
27
27
|
const captures = suiteTitle.match(/@S([\w\d]+)/);
|
|
@@ -63,12 +63,12 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
63
63
|
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
64
64
|
// .map(l => l.split(':')[0])
|
|
65
65
|
.map(l => l.trim())
|
|
66
|
-
.map(l => l.split(' ').find(p => p.includes(':')))
|
|
67
|
-
.filter(l => isValid(l
|
|
66
|
+
.map(l => l.split(' ').find(p => p.includes(':')) || '')
|
|
67
|
+
.filter(l => isValid(l?.split(':')[0]))
|
|
68
68
|
|
|
69
69
|
// // filter out 3rd party libs
|
|
70
|
-
.filter(l => !l
|
|
71
|
-
.filter(l => !l
|
|
70
|
+
.filter(l => !l?.includes(`vendor${ sep}`))
|
|
71
|
+
.filter(l => !l?.includes(`node_modules${ sep}`))
|
|
72
72
|
.filter(l => fs.existsSync(l.split(':')[0]))
|
|
73
73
|
.filter(l => fs.lstatSync(l.split(':')[0]).isFile())
|
|
74
74
|
|
|
@@ -79,6 +79,8 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
79
79
|
const prepend = 3;
|
|
80
80
|
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
|
|
81
81
|
|
|
82
|
+
if (!source) return '';
|
|
83
|
+
|
|
82
84
|
return source.split('\n')
|
|
83
85
|
.map((l, i) => {
|
|
84
86
|
if (i === prepend) return `${line} > ${chalk.bold(l)}`;
|
|
@@ -137,15 +139,46 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
const isSameTest = (test, t) =>
|
|
142
|
+
const isSameTest = (test, t) => (typeof t === 'object')
|
|
143
|
+
&& (typeof test === 'object')
|
|
144
|
+
&& t.title === test.title
|
|
145
|
+
&& t.suite_title === test.suite_title
|
|
146
|
+
&& Object.values(t.example || {}) === Object.values(test.example || {})
|
|
147
|
+
&& t.test_id === test.test_id;
|
|
148
|
+
|
|
149
|
+
const getCurrentDateTime = () => {
|
|
150
|
+
const today = new Date();
|
|
151
|
+
|
|
152
|
+
return `${today.getFullYear() }_${ today.getMonth() + 1 }_${ today.getDate() }_${
|
|
153
|
+
today.getHours() }_${ today.getMinutes() }_${ today.getSeconds()}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {Object} test - Test adapter object
|
|
158
|
+
*
|
|
159
|
+
* @returns {String|null} testInfo as one string
|
|
160
|
+
*/
|
|
161
|
+
const specificTestInfo = test => {
|
|
162
|
+
// TODO: afterEach has another context.... need to add specific handler, maybe...
|
|
163
|
+
if (test?.title && test?.file) {
|
|
164
|
+
|
|
165
|
+
return `${basename(test.file).split(".").join("#")
|
|
166
|
+
}#${
|
|
167
|
+
test.title.split(" ").join("#")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
141
172
|
|
|
142
173
|
module.exports = {
|
|
143
|
-
parseTest,
|
|
144
|
-
parseSuite,
|
|
145
|
-
ansiRegExp,
|
|
146
174
|
isSameTest,
|
|
147
|
-
isValidUrl,
|
|
148
175
|
fetchSourceCode,
|
|
149
176
|
fetchSourceCodeFromStackTrace,
|
|
150
177
|
fetchFilesFromStackTrace,
|
|
151
|
-
|
|
178
|
+
getCurrentDateTime,
|
|
179
|
+
specificTestInfo,
|
|
180
|
+
isValidUrl,
|
|
181
|
+
ansiRegExp,
|
|
182
|
+
parseTest,
|
|
183
|
+
parseSuite
|
|
184
|
+
}
|