@testomatio/reporter 0.8.0-beta.9 → 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.
@@ -1,4 +1,6 @@
1
- const AWS = require('aws-sdk');
1
+ const debug = require('debug')('@testomatio/reporter:file-uploader');
2
+ const { S3 } = require('@aws-sdk/client-s3');
3
+ const { Upload } = require('@aws-sdk/lib-storage');
2
4
  const fs = require('fs');
3
5
  const path = require('path');
4
6
  const chalk = require('chalk');
@@ -6,18 +8,38 @@ const { randomUUID } = require('crypto');
6
8
  const memoize = require('lodash.memoize');
7
9
  const { APP_PREFIX } = require('./constants');
8
10
 
9
- const {
10
- S3_ENDPOINT,
11
- S3_REGION,
12
- S3_BUCKET,
13
- S3_ACCESS_KEY_ID,
14
- S3_SECRET_ACCESS_KEY,
15
- TESTOMATIO_DISABLE_ARTIFACTS,
16
- TESTOMATIO_PRIVATE_ARTIFACTS,
17
- S3_FORCE_PATH_STYLE,
18
- } = process.env;
19
-
20
- const isArtifactsEnabled = S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS;
11
+ const keys = [
12
+ 'S3_ENDPOINT',
13
+ 'S3_REGION',
14
+ 'S3_BUCKET',
15
+ 'S3_ACCESS_KEY_ID',
16
+ 'S3_SECRET_ACCESS_KEY',
17
+ 'TESTOMATIO_DISABLE_ARTIFACTS',
18
+ 'TESTOMATIO_PRIVATE_ARTIFACTS',
19
+ 'S3_FORCE_PATH_STYLE',
20
+ ];
21
+
22
+ let config;
23
+
24
+ function getConfig() {
25
+ if (config) return config;
26
+ config = keys.reduce((acc, key) => {
27
+ acc[key] = process.env[key];
28
+ return acc;
29
+ }, {});
30
+ debug('Config', config);
31
+ return config;
32
+ }
33
+
34
+ let isEnabled;
35
+
36
+ const isArtifactsEnabled = () => {
37
+ if (isEnabled !== undefined) return isEnabled;
38
+ const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = getConfig();
39
+ isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
40
+ debug(`Upload is ${isEnabled ? 'enabled' : 'disabled'}`);
41
+ return isEnabled;
42
+ };
21
43
 
22
44
  const _getFileExtBase64 = str => {
23
45
  const type = str.charAt(0);
@@ -32,6 +54,25 @@ const _getFileExtBase64 = str => {
32
54
  );
33
55
  };
34
56
 
57
+ const _getS3Config = () => {
58
+ const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
59
+
60
+ const cfg = {
61
+ region: S3_REGION,
62
+ credentials: {
63
+ accessKeyId: S3_ACCESS_KEY_ID,
64
+ secretAccessKey: S3_SECRET_ACCESS_KEY,
65
+ s3ForcePathStyle: S3_FORCE_PATH_STYLE,
66
+ }
67
+ };
68
+
69
+ if (S3_ENDPOINT) {
70
+ cfg.endpoint = S3_ENDPOINT;
71
+ }
72
+
73
+ return cfg;
74
+ }
75
+
35
76
  const uploadUsingS3 = async (filePath, runId) => {
36
77
  let ContentType;
37
78
  let Key;
@@ -42,40 +83,39 @@ const uploadUsingS3 = async (filePath, runId) => {
42
83
  Key = filePath.name;
43
84
  }
44
85
 
45
- const config = {
46
- region: S3_REGION,
47
- accessKeyId: S3_ACCESS_KEY_ID,
48
- secretAccessKey: S3_SECRET_ACCESS_KEY,
49
- s3ForcePathStyle: S3_FORCE_PATH_STYLE,
50
- };
51
-
52
- if (S3_ENDPOINT) {
53
- config.endpoint = new AWS.Endpoint(S3_ENDPOINT);
54
- }
55
-
56
- const s3 = new AWS.S3(config);
57
86
  if (!fs.existsSync(filePath)) {
58
87
  console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
59
88
  return;
60
89
  }
90
+
91
+ const {
92
+ S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
93
+ } = getConfig();
61
94
 
62
95
  const file = fs.readFileSync(filePath);
63
96
 
64
97
  Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
65
98
  const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
66
99
 
100
+ const s3 = new S3(_getS3Config());
101
+
67
102
  try {
68
- const out = await s3
69
- .upload({
103
+ const out = new Upload({
104
+ client: s3,
105
+
106
+ params: {
70
107
  Bucket: S3_BUCKET,
71
108
  Key,
72
109
  Body: file,
73
110
  ContentType,
74
111
  ACL,
75
- })
76
- .promise();
112
+ }
113
+ });
77
114
 
78
- return out.Location;
115
+ await out.done();
116
+ debug('Uploaded', out.singleUploadResult.Location)
117
+
118
+ return out.singleUploadResult.Location;
79
119
  } catch (e) {
80
120
  console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
81
121
  accessKeyId: S3_ACCESS_KEY_ID,
@@ -100,35 +140,32 @@ const uploadUsingS3 = async (filePath, runId) => {
100
140
  };
101
141
 
102
142
  const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
103
- const config = {
104
- region: S3_REGION,
105
- accessKeyId: S3_ACCESS_KEY_ID,
106
- secretAccessKey: S3_SECRET_ACCESS_KEY,
107
- s3ForcePathStyle: S3_FORCE_PATH_STYLE,
108
- };
109
-
110
- if (S3_ENDPOINT) {
111
- config.endpoint = new AWS.Endpoint(S3_ENDPOINT);
112
- }
113
143
 
114
- const s3 = new AWS.S3(config);
144
+ const {
145
+ S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
146
+ } = getConfig();
115
147
 
116
148
  const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
117
149
 
118
150
  const fileExtension = _getFileExtBase64(buffer.toString('base64'));
119
151
  const Key = `${runId}/${fileName}${fileExtension}`;
120
152
 
153
+ const s3 = new S3(_getS3Config());
154
+
121
155
  try {
122
- const out = await s3
123
- .upload({
156
+ const out = new Upload({
157
+ client: s3,
158
+
159
+ params: {
124
160
  Bucket: S3_BUCKET,
125
161
  Key,
126
162
  Body: buffer,
127
163
  ACL,
128
- })
129
- .promise();
164
+ }
165
+ });
166
+ await out.done();
130
167
 
131
- return out.Location;
168
+ return out.singleUploadResult.Location;
132
169
  } catch (e) {
133
170
  console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
134
171
  accessKeyId: S3_ACCESS_KEY_ID,
@@ -154,7 +191,7 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
154
191
 
155
192
  const uploadFileByPath = async (filePath, runId) => {
156
193
  try {
157
- if (isArtifactsEnabled) {
194
+ if (isArtifactsEnabled()) {
158
195
  return uploadUsingS3(filePath, runId);
159
196
  }
160
197
  } catch (e) {
@@ -164,7 +201,7 @@ const uploadFileByPath = async (filePath, runId) => {
164
201
 
165
202
  const uploadFileAsBuffer = async (buffer, fileName, runId) => {
166
203
  try {
167
- if (isArtifactsEnabled) {
204
+ if (isArtifactsEnabled()) {
168
205
  return uploadUsingS3AsBuffer(buffer, fileName, runId);
169
206
  }
170
207
  } catch (e) {
@@ -175,5 +212,5 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
175
212
  module.exports = {
176
213
  uploadFileByPath: memoize(uploadFileByPath),
177
214
  uploadFileAsBuffer: memoize(uploadFileAsBuffer),
178
- isArtifactsEnabled,
215
+ isArtifactsEnabled: memoize(isArtifactsEnabled),
179
216
  };
@@ -14,5 +14,4 @@ class CSharpAdapter extends Adapter {
14
14
  }
15
15
  }
16
16
 
17
-
18
17
  module.exports = CSharpAdapter;
@@ -5,8 +5,7 @@ const PythonAdapter = require('./python');
5
5
  const RubyAdapter = require('./ruby');
6
6
  const CSharpAdapter = require('./csharp');
7
7
 
8
-
9
- module.exports = function (lang, opts) {
8
+ function AdapterFactory(lang, opts) {
10
9
  if (!lang) return new Adapter(opts);
11
10
 
12
11
  if (lang === 'java') {
@@ -25,3 +24,5 @@ module.exports = function (lang, opts) {
25
24
  return new CSharpAdapter(opts);
26
25
  }
27
26
  }
27
+
28
+ module.exports = AdapterFactory;
@@ -0,0 +1,133 @@
1
+ const debug = require('debug')('@testomatio/reporter:pipe:csv');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const csvWriter = require('csv-writer');
5
+ const chalk = require('chalk');
6
+ const merge = require('lodash.merge');
7
+ const { isSameTest, getCurrentDateTime } = require('../util');
8
+ const { CSV_HEADERS } = require('../constants');
9
+
10
+ /**
11
+ * @typedef {import('../../types').Pipe} Pipe
12
+ * @typedef {import('../../types').TestData} TestData
13
+ * @class CsvPipe
14
+ * @implements {Pipe}
15
+ */
16
+ class CsvPipe {
17
+
18
+ constructor(params, store) {
19
+ this.store = store || {};
20
+ this.title = params.title || process.env.TESTOMATIO_TITLE;
21
+ this.isEnabled = true;
22
+ this.results = [];
23
+
24
+ this.outputDir = 'export';
25
+ this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
26
+ this.isCsvSave = false;
27
+
28
+ if (this.csvFilename !== undefined && this.csvFilename.split('.').length > 0) {
29
+ this.isCsvSave = true;
30
+
31
+ if (this.csvFilename.split('.')[0] === 'report') {
32
+ this.outputFile = path.resolve(process.cwd(), this.outputDir, 'report.csv');
33
+ } else {
34
+ this.outputFile = path.resolve(
35
+ process.cwd(),
36
+ this.outputDir,
37
+ `${getCurrentDateTime()}_${this.csvFilename.split('.')[0]}.csv`,
38
+ );
39
+ }
40
+ }
41
+ }
42
+
43
+ async createRun() {
44
+ // empty
45
+ }
46
+
47
+ updateRun() {}
48
+
49
+ /**
50
+ * Create a folder that will contain the exported files
51
+ */
52
+ checkExportDir() {
53
+ if (!fs.existsSync(this.outputDir)) {
54
+ return fs.mkdirSync(this.outputDir);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Save data to the csv file.
60
+ * @param {Object} data - data that will be added to the CSV file.
61
+ * Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
62
+ * @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
63
+ */
64
+ async saveToCsv(data, headers) {
65
+ debug('Data', data);
66
+ // First, we check whether the export directory exists: if yes - OK, no - create it.
67
+ this.checkExportDir();
68
+
69
+ if (!this.outputFile) {
70
+ console.log(this, chalk.yellow(`CSV file is not set, ignoring`));
71
+ return;
72
+ }
73
+
74
+ console.log(this, chalk.yellow(`The test results will be added to the csv. It will take some time...`));
75
+ // Create csv writer object
76
+ const writer = csvWriter.createObjectCsvWriter({
77
+ path: this.outputFile,
78
+ header: headers,
79
+ });
80
+ // Save csv file based on the current data
81
+ try {
82
+ await writer.writeRecords(data);
83
+ console.log(this, chalk.green(`Recording completed! You can check the result in file = ${this.outputFile}`));
84
+ } catch (e) {
85
+ console.log('Unknown error: ', e);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Add test data to the result array for saving. As a result of this function, we get a result object to save.
91
+ * @param {Object} test - object which includes each test entry.
92
+ */
93
+ addTest(test) {
94
+ if (!this.isEnabled) return;
95
+
96
+ const index = this.results.findIndex(t => isSameTest(t, test));
97
+ // update if they were already added
98
+ if (index >= 0) {
99
+ this.results[index] = merge(this.results[index], test);
100
+ return;
101
+ }
102
+
103
+ const { suite_title, title, status, message, stack } = test;
104
+
105
+ this.results.push({
106
+ suite_title,
107
+ title,
108
+ status,
109
+ message,
110
+ stack,
111
+ });
112
+ }
113
+
114
+ /**
115
+ * @param {{ tests?: TestData[] }} runParams
116
+ * @returns {Promise<void>}
117
+ */
118
+ async finishRun(runParams) {
119
+ if (!this.isEnabled) return;
120
+
121
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
122
+ // Save results based on the default headers
123
+ if (this.isCsvSave) {
124
+ this.saveToCsv(this.results, CSV_HEADERS);
125
+ }
126
+ }
127
+
128
+ toString() {
129
+ return 'csv exporter';
130
+ }
131
+ }
132
+
133
+ module.exports = CsvPipe;
@@ -1,65 +1,68 @@
1
+ const debug = require('debug')('@testomatio/reporter:pipe:github');
2
+ const path = require('path');
1
3
  const chalk = require('chalk');
2
- const { Octokit } = require("@octokit/rest");
4
+ const humanizeDuration = require('humanize-duration');
5
+ const merge = require('lodash.merge');
6
+ const { Octokit } = require('@octokit/rest');
3
7
  const { APP_PREFIX } = require('../constants');
4
8
  const { ansiRegExp, isSameTest } = require('../util');
5
- const merge = require('lodash.merge');
6
-
7
9
 
10
+ /**
11
+ * @typedef {import('../../types').Pipe} Pipe
12
+ * @typedef {import('../../types').TestData} TestData
13
+ * @class GitHubPipe
14
+ * @implements {Pipe}
15
+ */
8
16
  class GitHubPipe {
9
-
10
- isEnabled = false;
11
-
12
17
  constructor(params, store = {}) {
18
+ this.isEnabled = false;
13
19
  this.store = store;
14
- this.tests = []
20
+ this.tests = [];
15
21
  this.token = params.GH_PAT || process.env.GH_PAT;
16
- this.ref = process.env.GITHUB_REF
17
- this.repo = process.env.GITHUB_REPOSITORY
22
+ this.ref = process.env.GITHUB_REF;
23
+ this.repo = process.env.GITHUB_REPOSITORY;
24
+ this.hiddenCommentData = `<!--- testomat.io report ${process.env.GITHUB_WORKFLOW || ''} -->`;
18
25
 
19
- if (process.env.DEBUG) {
20
- console.log(APP_PREFIX, 'GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', this.ref, this.repo);
21
- }
26
+ debug('GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', 'Ref:', this.ref, 'Repo:', this.repo);
22
27
 
23
28
  if (!this.token || !this.ref || !this.repo) return;
24
29
  this.isEnabled = true;
25
30
  const matchedIssue = this.ref.match(/refs\/pull\/(\d+)\/merge/);
26
31
  if (!matchedIssue) return;
27
- this.issue = matchedIssue[1]
32
+ this.issue = parseInt(matchedIssue[1], 10);
33
+
28
34
  this.start = new Date();
29
35
 
30
- if (process.env.DEBUG) {
31
- console.log(APP_PREFIX, 'GitHub Pipe: Enabled');
32
- }
36
+ debug('GitHub Pipe: Enabled');
33
37
  }
34
-
35
- async createRun() {}
36
38
 
37
- updateRun() {}
39
+ async createRun() {}
38
40
 
39
41
  addTest(test) {
42
+ debug('Adding test:', test);
40
43
  if (!this.isEnabled) return;
41
44
 
42
- const index = this.tests.findIndex(t => isSameTest(t, test))
45
+ const index = this.tests.findIndex(t => isSameTest(t, test));
43
46
  // update if they were already added
44
47
  if (index >= 0) {
45
48
  this.tests[index] = merge(this.tests[index], test);
46
49
  return;
47
50
  }
48
51
 
49
- this.tests.push(test)
52
+ this.tests.push(test);
50
53
  }
51
54
 
52
55
  async finishRun(runParams) {
53
56
  if (!this.isEnabled) return;
57
+ if (!this.issue) return;
54
58
 
55
59
  if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
56
-
57
60
 
58
61
  this.octokit = new Octokit({
59
62
  auth: this.token,
60
63
  });
61
64
 
62
- const [owner, repo] = this.repo.split('/');
65
+ const [owner, repo] = (this.repo || '').split('/');
63
66
  if (!(owner || repo)) return;
64
67
 
65
68
  // ... create a comment on GitHub
@@ -67,15 +70,17 @@ class GitHubPipe {
67
70
  const failedCount = this.tests.filter(t => t.status === 'failed').length;
68
71
  const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
69
72
 
70
- let summary = `
71
- ### Run Report
73
+ let summary = `${this.hiddenCommentData}
72
74
 
73
- | | ${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
75
+ | [![Testomat.io Report](https://avatars.githubusercontent.com/u/59105116?s=36&v=4)](https://testomat.io) | ${
76
+ statusEmoji(runParams.status,)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
74
77
  | --- | --- |
75
78
  | Tests | ✔️ **${this.tests.length}** tests run |
76
- | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji('passed')} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
77
- | Duration | 🕐 **${parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)).toFixed(2)}**ms |
78
- `
79
+ | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
80
+ 'passed',
81
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
82
+ | Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
83
+ `;
79
84
  if (this.store.runUrl) {
80
85
  summary += `| Testomat.io Report | 📊 [Run #${this.store.runId}](${this.store.runUrl}) | `;
81
86
  }
@@ -92,71 +97,89 @@ class GitHubPipe {
92
97
  summary += `| Run Attempt | 🌒 \`${process.env.GITHUB_RUN_ATTEMPT}\` | `;
93
98
  }
94
99
  if (process.env.GITHUB_RUN_ID) {
95
- summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID} | `;
100
+ summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${
101
+ this.repo
102
+ }/actions/runs/${process.env.GITHUB_RUN_ID} | `;
96
103
  }
97
104
 
105
+ const failures = this.tests
106
+ .filter(t => t.status === 'failed')
107
+ .slice(0, 20)
108
+ .map(t => {
109
+ let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
110
+ text += '\n\n';
111
+ if (t.message)
112
+ text += `> ${t.message
113
+ .replace(/[^\x20-\x7E]/g, '')
114
+ .replace(ansiRegExp(), '')
115
+ .trim()}\n`;
116
+ if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
117
+
118
+ if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
119
+ t.artifacts
120
+ .filter(f => !!f)
121
+ .filter(f => f.endsWith('.png'))
122
+ .forEach(f => {
123
+ if (f.endsWith('.png')) {
124
+ text += `![](${f})\n`;
125
+ return text;
126
+ }
127
+ text += `[📄 ${path.basename(f)}](${f})\n`;
128
+ return text;
129
+ });
130
+ }
131
+
132
+ text += '\n---\n';
133
+
134
+ return text;
135
+ });
98
136
 
99
- const failures = this.tests.filter(t => t.status === 'failed').slice(0, 20).map(t => {
100
- let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
101
- text += "\n\n"
102
- if (t.message) text += "> " + t.message.replace(/[^\x20-\x7E]/g, '').replace(ansiRegExp(), '').trim() + "\n"
103
- if (t.stack) text += "```diff\n" + t.stack.replace(ansiRegExp(), '').trim() + "\n```\n";
104
-
105
- if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
106
- t.artifacts.filter(f => f.endsWith('.png')).forEach(f => {
107
- if (f.endsWith('.png')) return text+= `![](${f})\n`
108
- });
109
- }
110
-
111
- text += "\n---\n"
112
-
113
- return text;
114
- })
115
-
116
137
  let body = summary;
117
138
 
118
139
  if (failures.length) {
119
- body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n${failures.join('\n')} \n</details>\n`;
120
- }
121
-
122
- if (failures.length > 20) {
123
- body += "\n> Notice\n> Only first 20 failures shown*"
140
+ body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
141
+ if (failures.length > 20) {
142
+ body += '\n> Notice\n> Only first 20 failures shown*';
143
+ }
144
+ body += '\n\n</details>';
124
145
  }
125
146
 
126
147
  if (this.tests.length > 0) {
127
- body += "\n### 🐢 Slowest Tests\n\n"
128
- body += this.tests.sort((a, b) => b?.run_time - a?.run_time).slice(0, 5).map(t => {
129
- return `* ${fullName(t)} (${parseFloat(t.run_time).toFixed(2)}ms)`
130
- }).join('\n')
148
+ body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
149
+ body += this.tests
150
+ // eslint-disable-next-line no-unsafe-optional-chaining
151
+ .sort((a, b) => b?.run_time - a?.run_time)
152
+ .slice(0, 5)
153
+ .map(t => `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`)
154
+ .join('\n');
155
+ body += '\n</details>';
131
156
  }
157
+
132
158
 
159
+ await deletePreviousReport(this.octokit, owner, repo, this.issue, this.hiddenCommentData);
160
+
161
+ // add report as comment
133
162
  try {
163
+ debug('Adding comment\n', body);
134
164
  const resp = await this.octokit.rest.issues.createComment({
135
165
  owner,
136
166
  repo,
137
167
  issue_number: this.issue,
138
168
  body,
139
169
  });
140
-
170
+
141
171
  const url = resp.data?.html_url;
172
+ debug('Comment URL:', url);
142
173
  this.store.githubUrl = url;
143
-
144
- console.log(
145
- APP_PREFIX,
146
- chalk.yellow('GitHub'),
147
- `Report created: ${chalk.magenta(url)}`,
148
- );
174
+
175
+ console.log(APP_PREFIX, chalk.yellow('GitHub'), `Report created: ${chalk.magenta(url)}`);
149
176
  } catch (err) {
150
- console.log(
151
- APP_PREFIX,
152
- chalk.yellow('GitHub'),
153
- `Couldn't create GitHub report ${err}`,
154
- );
177
+ console.log(APP_PREFIX, chalk.yellow('GitHub'), `Couldn't create GitHub report ${err}`);
155
178
  }
156
179
  }
157
180
 
158
181
  toString() {
159
- return 'GitHub Reporter'
182
+ return 'GitHub Reporter';
160
183
  }
161
184
  }
162
185
 
@@ -164,15 +187,56 @@ function statusEmoji(status) {
164
187
  if (status === 'passed') return '🟢';
165
188
  if (status === 'failed') return '🔴';
166
189
  if (status === 'skipped') return '🟡';
167
- return ''
190
+ return '';
168
191
  }
169
192
 
170
193
  function fullName(t) {
171
194
  let line = '';
172
- if (t.suite_title) line = `${t.suite_title}: `;
173
- line += `**${t.title}**`
195
+ if (t.suite_title) line = `${t.suite_title}: `;
196
+ line += `**${t.title}**`;
174
197
  if (t.example) line += ` \`[${Object.values(t.example)}]\``;
175
198
  return line;
176
199
  }
177
200
 
178
- module.exports = GitHubPipe;
201
+ async function deletePreviousReport(octokit, owner, repo, issue, hiddenCommentData) {
202
+ if (process.env.GITHUB_KEEP_OUTDATED_REPORTS) return;
203
+
204
+ // get comments
205
+ let comments = [];
206
+ try {
207
+ const response = await octokit.rest.issues.listComments({
208
+ owner,
209
+ repo,
210
+ issue_number: issue,
211
+ });
212
+ comments = response.data;
213
+ } catch (e) {
214
+ console.error('Error while attempt to retrieve comments on GitHub Pull Request:\n', e);
215
+ }
216
+
217
+ if (!comments.length) return;
218
+
219
+ for (const comment of comments) {
220
+ // if comment was left by the same workflow
221
+ if (comment.body.includes(hiddenCommentData)) {
222
+ try {
223
+ // delete previous comment
224
+ await octokit.rest.issues.deleteComment({
225
+ owner,
226
+ repo,
227
+ issue_number: issue,
228
+ comment_id: comment.id,
229
+ });
230
+ } catch (e) {
231
+ console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
232
+ }
233
+
234
+ // pass next env var if need to clear all previous reports;
235
+ // only the last one is removed by default
236
+ if (!process.env.GITHUB_REMOVE_ALL_OUTDATED_REPORTS) break;
237
+ // TODO: in case of many reports should implement pagination
238
+ }
239
+ }
240
+ }
241
+
242
+ module.exports = GitHubPipe;