@testomatio/reporter 1.4.10 β†’ 1.4.11-beta.1-json

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 CHANGED
@@ -11,7 +11,7 @@ Testomat.io Reporter (this npm package) supports:
11
11
  - πŸ„ Integarion with all popular [JavaScript/TypeScript frameworks](./docs/frameworks.md)
12
12
  - πŸ—„οΈ Screenshots, videos, traces [uploaded into S3 bucket](./docs/artifacts.md)
13
13
  - πŸ”Ž [Stack traces](./docs/stacktrace.md) and error messages
14
- - πŸ™ [GitHub](./docs/pipes/github.md) & [GitLab](./docs/pipes/gitlab.md) integration
14
+ - πŸ™ [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
15
15
  - πŸš… Realtime reports
16
16
  - πŸ—ƒοΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
17
17
  - πŸšΆβ€β™€οΈ Steps _(work in progress)_
@@ -128,6 +128,7 @@ Bring this reporter on CI and never lose test results again!
128
128
  - [Gitlab](./docs/pipes/gitlab.md)
129
129
  - [CSV](./docs/pipes/csv.md)
130
130
  - [HTML report](./docs/pipes/html.md)
131
+ - [Bitbucket](./docs/pipes/bitbucket.md)
131
132
  - πŸ““ [JUnit](./docs/junit.md)
132
133
  - πŸ—„οΈ [Artifacts](./docs/artifacts.md)
133
134
  - πŸ”‚ [Workflows](./docs/workflows.md)
@@ -147,6 +147,9 @@ function checkStatus(status) {
147
147
  }
148
148
 
149
149
  function appendStep(step, shift = 0) {
150
+ // nesting too deep, ignore those steps
151
+ if (shift > 10) return;
152
+
150
153
  let newCategory = step.category;
151
154
  switch (newCategory) {
152
155
  case 'test.step':
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const { spawn } = require('child_process');
2
+ const spawn = require('cross-spawn');
3
3
  const program = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const TestomatClient = require('../client');
package/lib/client.js CHANGED
@@ -279,7 +279,10 @@ class Client {
279
279
  logs = logs?.trim();
280
280
 
281
281
  if (Array.isArray(steps)) {
282
- steps = steps.map(step => formatStep(step)).flat().join('\n');
282
+ steps = steps
283
+ .map(step => formatStep(step))
284
+ .flat()
285
+ .join('\n');
283
286
  }
284
287
 
285
288
  let testLogs = '';
@@ -358,8 +361,8 @@ function formatStep(step, shift = 0) {
358
361
 
359
362
  for (const child of step.steps || []) {
360
363
  lines.push(...formatStep(child, shift + 2));
361
- }
362
-
364
+ }
365
+
363
366
  return lines;
364
367
  }
365
368
 
@@ -0,0 +1,254 @@
1
+ const debug = require('debug')('@testomatio/reporter:pipe:bitbucket');
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, testomatLogoURL } = require('../constants');
8
+ const { ansiRegExp, isSameTest } = require('../utils/utils');
9
+ const { statusEmoji, fullName } = require('../utils/pipe_utils');
10
+
11
+ //! BITBUCKET_ACCESS_TOKEN environment variable is required for this functionality to work
12
+ //! and your pipeline trigger should be a pull request
13
+
14
+ /**
15
+ * @class BitbucketPipe
16
+ * @typedef {import('../../types').Pipe} Pipe
17
+ * @typedef {import('../../types').TestData} TestData
18
+ */
19
+ class BitbucketPipe {
20
+ constructor(params, store = {}) {
21
+ this.isEnabled = false;
22
+ this.ENV = process.env;
23
+ this.store = store;
24
+ this.tests = [];
25
+ // Bitbucket PAT looks like bbpat-*****
26
+ this.token = params.BITBUCKET_ACCESS_TOKEN || process.env.BITBUCKET_ACCESS_TOKEN || this.ENV.BITBUCKET_ACCESS_TOKEN;
27
+ this.hiddenCommentData = `Testomat.io report: ${process.env.BITBUCKET_BRANCH || ''}`;
28
+
29
+ debug(
30
+ chalk.yellow('Bitbucket Pipe:'),
31
+ this.token ? 'TOKEN passed' : '*no token*',
32
+ `Project key: ${this.ENV.BITBUCKET_PROJECT_KEY}, Pull request ID: ${this.ENV.BITBUCKET_PR_ID}`,
33
+ );
34
+
35
+ if (!this.token) {
36
+ debug(`Hint: Bitbucket CI variables are unavailable for unprotected branches by default.`);
37
+ return;
38
+ }
39
+
40
+ this.isEnabled = true;
41
+
42
+ debug('Bitbucket Pipe: Enabled');
43
+ }
44
+
45
+ async cleanLog(log) {
46
+ const stripAnsi = (await import('strip-ansi')).default;
47
+ return stripAnsi(log);
48
+ }
49
+
50
+ // Prepare the run (if needed)
51
+ async prepareRun() {}
52
+
53
+ // Create a new run (if needed)
54
+ async createRun() {}
55
+
56
+ addTest(test) {
57
+ if (!this.isEnabled) return;
58
+
59
+ const index = this.tests.findIndex(t => isSameTest(t, test));
60
+ // Update if they were already added
61
+ if (index >= 0) {
62
+ this.tests[index] = merge(this.tests[index], test);
63
+ return;
64
+ }
65
+
66
+ this.tests.push(test);
67
+ }
68
+
69
+ async finishRun(runParams) {
70
+ if (!this.isEnabled) return;
71
+
72
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
73
+
74
+ // Clean up the logs from ANSI codes
75
+ for (let i = 0; i < this.tests.length; i++) {
76
+ this.tests[i].message = await this.cleanLog(this.tests[i].message || '');
77
+ this.tests[i].stack = await this.cleanLog(this.tests[i].stack || '');
78
+ }
79
+
80
+ // Create a comment on Bitbucket
81
+ const passedCount = this.tests.filter(t => t.status === 'passed').length;
82
+ const failedCount = this.tests.filter(t => t.status === 'failed').length;
83
+ const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
84
+
85
+ // Constructing the table
86
+ let summary = `${this.hiddenCommentData}
87
+
88
+ | ![Testomat.io Report](${testomatLogoURL}) | ${statusEmoji(
89
+ runParams.status,
90
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
91
+ | --- | --- |
92
+ | **Tests** | βœ”οΈ **${this.tests.length}** tests run |
93
+ | **Summary** | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
94
+ 'passed',
95
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
96
+ | **Duration** | πŸ• **${humanizeDuration(
97
+ parseInt(
98
+ this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
99
+ 10,
100
+ ),
101
+ {
102
+ maxDecimalPoints: 0,
103
+ },
104
+ )}** |
105
+ `;
106
+
107
+ if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
108
+ // eslint-disable-next-line max-len
109
+ summary += `| **Job** | πŸ‘· [#${this.ENV.BITBUCKET_BUILD_NUMBER}](https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${this.ENV.BITBUCKET_BUILD_NUMBER}") by commit: **${this.ENV.BITBUCKET_COMMIT}** |`;
110
+ }
111
+
112
+ const failures = this.tests
113
+ .filter(t => t.status === 'failed')
114
+ .slice(0, 20)
115
+ .map(t => {
116
+ let text = `${statusEmoji('failed')} ${fullName(t)}\n`;
117
+ if (t.message) {
118
+ text += `> ${t.message
119
+ .replace(/[^\x20-\x7E]/g, '')
120
+ .replace(ansiRegExp(), '')
121
+ .trim()}\n`;
122
+ }
123
+ if (t.stack) {
124
+ text += `\n\`\`\`diff\n${t.stack
125
+ .replace(ansiRegExp(), '')
126
+ .replace(
127
+ /^[\s\S]*################\[ Failure \]################/g,
128
+ '################[ Failure ]################',
129
+ )
130
+ .trim()}\n\`\`\`\n`;
131
+ }
132
+ if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
133
+ t.artifacts
134
+ .filter(f => !!f)
135
+ .forEach(f => {
136
+ if (f.endsWith('.png')) {
137
+ text += `![Image](${f})\n`;
138
+ } else {
139
+ text += `[πŸ“„ ${path.basename(f)}](${f})\n`;
140
+ }
141
+ });
142
+ }
143
+ text += `\n---\n`;
144
+ return text;
145
+ });
146
+
147
+ let body = summary;
148
+
149
+ if (failures.length) {
150
+ body += `\nπŸŸ₯ **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
151
+ if (failures.length > 10) {
152
+ body += `\n> Notice: Only the first 10 failures are shown.`;
153
+ }
154
+ }
155
+
156
+ if (this.tests.length > 0) {
157
+ body += `\n\n**🐒 Slowest Tests**\n\n`;
158
+ body += this.tests
159
+ .sort((a, b) => b.run_time - a.run_time)
160
+ .slice(0, 5)
161
+ .map(t => `* **${fullName(t)}** (${humanizeDuration(parseFloat(t.run_time))})`)
162
+ .join('\n');
163
+ }
164
+
165
+ // Construct Bitbucket API URL for comments
166
+ // eslint-disable-next-line max-len
167
+ const commentsRequestURL = `https://api.bitbucket.org/2.0/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
168
+
169
+ // Delete previous report
170
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
171
+
172
+ // Add current report
173
+ debug(`Adding comment via URL: ${commentsRequestURL}`);
174
+ debug(`Final Bitbucket API call body: ${body}`);
175
+
176
+ try {
177
+ const addCommentResponse = await axios.post(
178
+ commentsRequestURL,
179
+ { content: { raw: body } },
180
+ {
181
+ headers: {
182
+ Authorization: `Bearer ${this.token}`,
183
+ 'Content-Type': 'application/json',
184
+ },
185
+ },
186
+ );
187
+
188
+ const commentID = addCommentResponse.data.id;
189
+ // eslint-disable-next-line max-len
190
+ const commentURL = `https://bitbucket.org/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pull-requests/${this.ENV.BITBUCKET_PR_ID}#comment-${commentID}`;
191
+
192
+ console.log(APP_PREFIX, chalk.yellow('Bitbucket'), `Report created: ${chalk.magenta(commentURL)}`);
193
+ } catch (err) {
194
+ console.error(
195
+ APP_PREFIX,
196
+ chalk.yellow('Bitbucket'),
197
+ `Couldn't create Bitbucket report\n${err}.
198
+ Request URL: ${commentsRequestURL}
199
+ Request data: ${body}`,
200
+ );
201
+ }
202
+ }
203
+
204
+ toString() {
205
+ return 'Bitbucket Reporter';
206
+ }
207
+
208
+ updateRun() {}
209
+ }
210
+
211
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
212
+ if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
213
+
214
+ // Get comments
215
+ let comments = [];
216
+
217
+ try {
218
+ const response = await axiosInstance.get(commentsRequestURL, {
219
+ headers: {
220
+ Authorization: `Bearer ${token}`,
221
+ 'Content-Type': 'application/json',
222
+ },
223
+ });
224
+ comments = response.data.values;
225
+ } catch (e) {
226
+ console.error('Error while attempting to retrieve comments on Bitbucket Pull Request:\n', e);
227
+ }
228
+
229
+ if (!comments.length) return;
230
+
231
+ for (const comment of comments) {
232
+ // If comment was left by the same workflow
233
+ if (comment.content.raw.includes(hiddenCommentData)) {
234
+ try {
235
+ // Delete previous comment
236
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
237
+ await axiosInstance.delete(deleteCommentURL, {
238
+ headers: {
239
+ Authorization: `Bearer ${token}`,
240
+ 'Content-Type': 'application/json',
241
+ },
242
+ });
243
+ } catch (e) {
244
+ console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
245
+ }
246
+ // Pass next env var if need to clear all previous reports;
247
+ // only the last one is removed by default
248
+ if (!process.env.BITBUCKET_REMOVE_ALL_OUTDATED_REPORTS) break;
249
+ // TODO: in case of many reports should implement pagination
250
+ }
251
+ }
252
+ }
253
+
254
+ module.exports = BitbucketPipe;
package/lib/pipe/index.js CHANGED
@@ -7,6 +7,7 @@ const GitHubPipe = require('./github');
7
7
  const GitLabPipe = require('./gitlab');
8
8
  const CsvPipe = require('./csv');
9
9
  const HtmlPipe = require('./html');
10
+ const BitbucketPipe = require('./bitbucket');
10
11
 
11
12
  function PipeFactory(params, opts) {
12
13
  const extraPipes = [];
@@ -45,6 +46,7 @@ function PipeFactory(params, opts) {
45
46
  new GitLabPipe(params, opts),
46
47
  new CsvPipe(params, opts),
47
48
  new HtmlPipe(params, opts),
49
+ new BitbucketPipe(params, opts),
48
50
  ...extraPipes,
49
51
  ];
50
52
 
@@ -310,6 +310,8 @@ class TestomatioPipe {
310
310
  const testsToSend = this.batch.tests.splice(0);
311
311
  debug('πŸ“¨ Batch upload', testsToSend.length, 'tests');
312
312
 
313
+ testsToSend.forEach(debug);
314
+
313
315
  return this.axios
314
316
  .post(
315
317
  `/api/reporter/${this.runId}/testrun`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.4.10",
3
+ "version": "1.4.11-beta.1-json",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -17,6 +17,7 @@
17
17
  "callsite-record": "^4.1.4",
18
18
  "chalk": "^4.1.0",
19
19
  "commander": "^4.1.1",
20
+ "cross-spawn": "^7.0.3",
20
21
  "csv-writer": "^1.6.0",
21
22
  "debug": "^4.3.4",
22
23
  "dotenv": "^16.0.1",
@@ -32,6 +33,7 @@
32
33
  "lodash.merge": "^4.6.2",
33
34
  "minimatch": "^9.0.3",
34
35
  "promise-retry": "^2.0.1",
36
+ "strip-ansi": "^7.1.0",
35
37
  "uuid": "^9.0.0"
36
38
  },
37
39
  "files": [