@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.
Files changed (97) hide show
  1. package/lib/adapter/codecept.js +288 -330
  2. package/lib/adapter/cucumber/current.js +195 -203
  3. package/lib/adapter/cucumber/legacy.js +130 -155
  4. package/lib/adapter/cucumber.js +5 -16
  5. package/lib/adapter/cypress-plugin/index.js +91 -105
  6. package/lib/adapter/jasmine/jasmine.js +63 -0
  7. package/lib/adapter/jasmine.js +54 -53
  8. package/lib/adapter/jest.js +97 -99
  9. package/lib/adapter/mocha/mocha.js +125 -0
  10. package/lib/adapter/mocha.js +111 -140
  11. package/lib/adapter/playwright.js +168 -200
  12. package/lib/adapter/vitest.js +144 -143
  13. package/lib/adapter/webdriver.js +113 -97
  14. package/lib/bin/reportXml.js +49 -49
  15. package/lib/bin/startTest.js +80 -97
  16. package/lib/client.js +344 -385
  17. package/lib/config.js +16 -21
  18. package/lib/constants.js +49 -43
  19. package/lib/data-storage.js +206 -188
  20. package/lib/fileUploader.js +245 -0
  21. package/lib/junit-adapter/adapter.js +17 -20
  22. package/lib/junit-adapter/csharp.js +18 -14
  23. package/lib/junit-adapter/index.js +27 -25
  24. package/lib/junit-adapter/java.js +41 -53
  25. package/lib/junit-adapter/javascript.js +30 -27
  26. package/lib/junit-adapter/python.js +38 -37
  27. package/lib/junit-adapter/ruby.js +11 -8
  28. package/lib/output.js +44 -52
  29. package/lib/package.json +1 -0
  30. package/lib/pipe/bitbucket.js +208 -227
  31. package/lib/pipe/csv.js +111 -124
  32. package/lib/pipe/github.js +184 -211
  33. package/lib/pipe/gitlab.js +164 -205
  34. package/lib/pipe/html.js +253 -312
  35. package/lib/pipe/index.js +83 -63
  36. package/lib/pipe/testomatio.js +391 -454
  37. package/lib/reporter-functions.js +16 -20
  38. package/lib/reporter.js +47 -17
  39. package/lib/services/artifacts.js +55 -51
  40. package/lib/services/index.js +14 -12
  41. package/lib/services/key-values.js +56 -53
  42. package/lib/services/logger.js +227 -245
  43. package/lib/utils/chalk.js +10 -0
  44. package/lib/utils/pipe_utils.js +91 -84
  45. package/lib/utils/utils.js +289 -273
  46. package/lib/xmlReader.js +480 -519
  47. package/package.json +57 -19
  48. package/src/adapter/codecept.js +369 -0
  49. package/src/adapter/cucumber/current.js +228 -0
  50. package/src/adapter/cucumber/legacy.js +158 -0
  51. package/src/adapter/cucumber.js +4 -0
  52. package/src/adapter/cypress-plugin/index.js +110 -0
  53. package/src/adapter/jasmine.js +60 -0
  54. package/src/adapter/jest.js +107 -0
  55. package/src/adapter/mocha.cjs +2 -0
  56. package/src/adapter/mocha.js +156 -0
  57. package/src/adapter/playwright.js +222 -0
  58. package/src/adapter/vitest.js +183 -0
  59. package/src/adapter/webdriver.js +111 -0
  60. package/src/bin/reportXml.js +67 -0
  61. package/src/bin/startTest.js +119 -0
  62. package/src/client.js +423 -0
  63. package/src/config.js +30 -0
  64. package/src/constants.js +49 -0
  65. package/src/data-storage.js +204 -0
  66. package/src/fileUploader.js +307 -0
  67. package/src/junit-adapter/adapter.js +23 -0
  68. package/src/junit-adapter/csharp.js +16 -0
  69. package/src/junit-adapter/index.js +28 -0
  70. package/src/junit-adapter/java.js +58 -0
  71. package/src/junit-adapter/javascript.js +31 -0
  72. package/src/junit-adapter/python.js +42 -0
  73. package/src/junit-adapter/ruby.js +10 -0
  74. package/src/output.js +57 -0
  75. package/src/pipe/bitbucket.js +254 -0
  76. package/src/pipe/csv.js +140 -0
  77. package/src/pipe/github.js +234 -0
  78. package/src/pipe/gitlab.js +229 -0
  79. package/src/pipe/html.js +366 -0
  80. package/src/pipe/index.js +73 -0
  81. package/src/pipe/testomatio.js +498 -0
  82. package/src/reporter-functions.js +44 -0
  83. package/src/reporter.cjs +22 -0
  84. package/src/reporter.js +24 -0
  85. package/src/services/artifacts.js +59 -0
  86. package/src/services/index.js +13 -0
  87. package/src/services/key-values.js +59 -0
  88. package/src/services/logger.js +314 -0
  89. package/src/template/emptyData.svg +23 -0
  90. package/src/template/testomatio.hbs +1421 -0
  91. package/src/utils/chalk.js +13 -0
  92. package/src/utils/pipe_utils.js +127 -0
  93. package/src/utils/utils.js +341 -0
  94. package/src/xmlReader.js +551 -0
  95. package/lib/bin/cli.js +0 -216
  96. package/lib/bin/uploadArtifacts.js +0 -86
  97. package/lib/uploader.js +0 -312
@@ -0,0 +1,42 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import Adapter from './adapter.js';
4
+
5
+ class PythonAdapter extends Adapter {
6
+ getFilePath(t) {
7
+ let fileName = namespaceToFileName(t.suite_title, { checkFile: true });
8
+ if (!fileName) fileName = namespaceToFileName(t.suite_title, { checkFile: false });
9
+ return fileName;
10
+ }
11
+
12
+ formatTest(t) {
13
+ const fileParts = t.suite_title.split('.');
14
+ const example = t.title.match(/\[(.*)\]/)?.[1];
15
+ if (example) t.example = { '#': example };
16
+
17
+ t.file = namespaceToFileName(t.suite_title);
18
+ t.title = t.title.split('[')[0];
19
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ');
20
+ return t;
21
+ }
22
+
23
+ formatMessage(t) {
24
+ return t.message.split('
')[0];
25
+ }
26
+ }
27
+
28
+ function namespaceToFileName(fileName, opts = {}) {
29
+ const fileParts = fileName.split('.');
30
+
31
+ while (fileParts.length > 0) {
32
+ const file = `${fileParts.join(path.sep)}.py`;
33
+ if (!opts.checkFile) return file;
34
+ if (fs.existsSync(`${fileParts.join(path.sep)}.py`)) {
35
+ return file;
36
+ }
37
+ fileParts.pop();
38
+ }
39
+ return null;
40
+ }
41
+
42
+ export default PythonAdapter;
@@ -0,0 +1,10 @@
1
+ import Adapter from './adapter.js';
2
+
3
+ class RubyAdapter extends Adapter {
4
+ formatStack(t) {
5
+ const stack = super.formatStack(t);
6
+ return stack.replace(/\[(.*?:.\d*)\]/g, '\n$1');
7
+ }
8
+ }
9
+
10
+ export default RubyAdapter;
package/src/output.js ADDED
@@ -0,0 +1,57 @@
1
+ import { format } from 'util';
2
+
3
+ const consoleLog = console.log;
4
+ const consoleError = console.error;
5
+
6
+ const formatArgs = args => format.apply(format, Array.prototype.slice.call(args));
7
+
8
+ class Output {
9
+ constructor(opts = {}) {
10
+ this.filterFn = opts.filterFn || (() => true);
11
+ this.reset();
12
+ }
13
+
14
+ reset() {
15
+ this.log = [];
16
+ this.stop(); // resotre console log if it was overridden
17
+ }
18
+
19
+ start() {
20
+ const { filterFn } = this;
21
+ const self = this;
22
+ console.log = (...args) => {
23
+ const obj = {};
24
+ Error.captureStackTrace(obj);
25
+ const logString = formatArgs(args);
26
+ if (filterFn(obj.stack)) {
27
+ self.log.push(logString);
28
+ }
29
+ consoleLog(logString);
30
+ };
31
+
32
+ console.error = (...args) => {
33
+ const obj = {};
34
+ Error.captureStackTrace(obj);
35
+ const logString = formatArgs(args);
36
+ if (filterFn(obj.stack)) {
37
+ self.log.push(logString);
38
+ }
39
+ consoleError(logString);
40
+ };
41
+ }
42
+
43
+ push(line) {
44
+ this.log.push(line);
45
+ }
46
+
47
+ text() {
48
+ return this.log.join('\n');
49
+ }
50
+
51
+ stop() {
52
+ console.log = consoleLog;
53
+ console.error = consoleError;
54
+ }
55
+ }
56
+
57
+ export default Output;
@@ -0,0 +1,254 @@
1
+ import { APP_PREFIX, testomatLogoURL } from '../constants.js';
2
+ import { ansiRegExp, isSameTest } from '../utils/utils.js';
3
+ import { statusEmoji, fullName } from '../utils/pipe_utils.js';
4
+ import axios from 'axios';
5
+ import pc from 'picocolors';
6
+ import humanizeDuration from 'humanize-duration';
7
+ import merge from 'lodash.merge';
8
+ import path from 'path';
9
+ import createDebugMessages from 'debug';
10
+
11
+ const debug = createDebugMessages('@testomatio/reporter:pipe:bitbucket');
12
+
13
+ //! BITBUCKET_ACCESS_TOKEN environment variable is required for this functionality to work
14
+ //! and your pipeline trigger should be a pull request
15
+
16
+ /**
17
+ * @class BitbucketPipe
18
+ * @typedef {import('../../types').Pipe} Pipe
19
+ * @typedef {import('../../types').TestData} TestData
20
+ */
21
+ export class BitbucketPipe {
22
+ constructor(params, store = {}) {
23
+ this.isEnabled = false;
24
+ this.ENV = process.env;
25
+ this.store = store;
26
+ this.tests = [];
27
+ // Bitbucket PAT looks like bbpat-*****
28
+ this.token = params.BITBUCKET_ACCESS_TOKEN || process.env.BITBUCKET_ACCESS_TOKEN || this.ENV.BITBUCKET_ACCESS_TOKEN;
29
+ this.hiddenCommentData = `Testomat.io report: ${process.env.BITBUCKET_BRANCH || ''}`;
30
+
31
+ debug(
32
+ pc.yellow('Bitbucket Pipe:'),
33
+ this.token ? 'TOKEN passed' : '*no token*',
34
+ `Project key: ${this.ENV.BITBUCKET_PROJECT_KEY}, Pull request ID: ${this.ENV.BITBUCKET_PR_ID}`,
35
+ );
36
+
37
+ if (!this.token) {
38
+ debug(`Hint: Bitbucket CI variables are unavailable for unprotected branches by default.`);
39
+ return;
40
+ }
41
+
42
+ this.isEnabled = true;
43
+
44
+ debug('Bitbucket Pipe: Enabled');
45
+ }
46
+
47
+ async cleanLog(log) {
48
+ const stripAnsi = (await import('strip-ansi')).default;
49
+ return stripAnsi(log);
50
+ }
51
+
52
+ // Prepare the run (if needed)
53
+ async prepareRun() {}
54
+
55
+ // Create a new run (if needed)
56
+ async createRun() {}
57
+
58
+ addTest(test) {
59
+ if (!this.isEnabled) return;
60
+
61
+ const index = this.tests.findIndex(t => isSameTest(t, test));
62
+ // Update if they were already added
63
+ if (index >= 0) {
64
+ this.tests[index] = merge(this.tests[index], test);
65
+ return;
66
+ }
67
+
68
+ this.tests.push(test);
69
+ }
70
+
71
+ async finishRun(runParams) {
72
+ if (!this.isEnabled) return;
73
+
74
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
75
+
76
+ // Clean up the logs from ANSI codes
77
+ for (let i = 0; i < this.tests.length; i++) {
78
+ this.tests[i].message = await this.cleanLog(this.tests[i].message || '');
79
+ this.tests[i].stack = await this.cleanLog(this.tests[i].stack || '');
80
+ }
81
+
82
+ // Create a comment on Bitbucket
83
+ const passedCount = this.tests.filter(t => t.status === 'passed').length;
84
+ const failedCount = this.tests.filter(t => t.status === 'failed').length;
85
+ const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
86
+
87
+ // Constructing the table
88
+ let summary = `${this.hiddenCommentData}
89
+
90
+ | ![Testomat.io Report](${testomatLogoURL}) | ${statusEmoji(
91
+ runParams.status,
92
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
93
+ | --- | --- |
94
+ | **Tests** | ✔️ **${this.tests.length}** tests run |
95
+ | **Summary** | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
96
+ 'passed',
97
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
98
+ | **Duration** | 🕐 **${humanizeDuration(
99
+ parseInt(
100
+ this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
101
+ 10,
102
+ ),
103
+ {
104
+ maxDecimalPoints: 0,
105
+ },
106
+ )}** |
107
+ `;
108
+
109
+ if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
110
+ // eslint-disable-next-line max-len
111
+ 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}** |`;
112
+ }
113
+
114
+ const failures = this.tests
115
+ .filter(t => t.status === 'failed')
116
+ .slice(0, 20)
117
+ .map(t => {
118
+ let text = `${statusEmoji('failed')} ${fullName(t)}\n`;
119
+ if (t.message) {
120
+ text += `> ${t.message
121
+ .replace(/[^\x20-\x7E]/g, '')
122
+ .replace(ansiRegExp(), '')
123
+ .trim()}\n`;
124
+ }
125
+ if (t.stack) {
126
+ text += `\n\`\`\`diff\n${t.stack
127
+ .replace(ansiRegExp(), '')
128
+ .replace(
129
+ /^[\s\S]*################\[ Failure \]################/g,
130
+ '################[ Failure ]################',
131
+ )
132
+ .trim()}\n\`\`\`\n`;
133
+ }
134
+ if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
135
+ t.artifacts
136
+ .filter(f => !!f)
137
+ .forEach(f => {
138
+ if (f.endsWith('.png')) {
139
+ text += `![Image](${f})\n`;
140
+ } else {
141
+ text += `[📄 ${path.basename(f)}](${f})\n`;
142
+ }
143
+ });
144
+ }
145
+ text += `\n---\n`;
146
+ return text;
147
+ });
148
+
149
+ let body = summary;
150
+
151
+ if (failures.length) {
152
+ body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
153
+ if (failures.length > 10) {
154
+ body += `\n> Notice: Only the first 10 failures are shown.`;
155
+ }
156
+ }
157
+
158
+ if (this.tests.length > 0) {
159
+ body += `\n\n**🐢 Slowest Tests**\n\n`;
160
+ body += this.tests
161
+ .sort((a, b) => b.run_time - a.run_time)
162
+ .slice(0, 5)
163
+ .map(t => `* **${fullName(t)}** (${humanizeDuration(parseFloat(t.run_time))})`)
164
+ .join('\n');
165
+ }
166
+
167
+ // Construct Bitbucket API URL for comments
168
+ // eslint-disable-next-line max-len
169
+ 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`;
170
+
171
+ // Delete previous report
172
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
173
+
174
+ // Add current report
175
+ debug(`Adding comment via URL: ${commentsRequestURL}`);
176
+ debug(`Final Bitbucket API call body: ${body}`);
177
+
178
+ try {
179
+ const addCommentResponse = await axios.post(
180
+ commentsRequestURL,
181
+ { content: { raw: body } },
182
+ {
183
+ headers: {
184
+ Authorization: `Bearer ${this.token}`,
185
+ 'Content-Type': 'application/json',
186
+ },
187
+ },
188
+ );
189
+
190
+ const commentID = addCommentResponse.data.id;
191
+ // eslint-disable-next-line max-len
192
+ const commentURL = `https://bitbucket.org/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pull-requests/${this.ENV.BITBUCKET_PR_ID}#comment-${commentID}`;
193
+
194
+ console.log(APP_PREFIX, pc.yellow('Bitbucket'), `Report created: ${pc.magenta(commentURL)}`);
195
+ } catch (err) {
196
+ console.error(
197
+ APP_PREFIX,
198
+ pc.yellow('Bitbucket'),
199
+ `Couldn't create Bitbucket report\n${err}.
200
+ Request URL: ${commentsRequestURL}
201
+ Request data: ${body}`,
202
+ );
203
+ }
204
+ }
205
+
206
+ toString() {
207
+ return 'Bitbucket Reporter';
208
+ }
209
+
210
+ updateRun() {}
211
+ }
212
+
213
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
214
+ if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
215
+
216
+ // Get comments
217
+ let comments = [];
218
+
219
+ try {
220
+ const response = await axiosInstance.get(commentsRequestURL, {
221
+ headers: {
222
+ Authorization: `Bearer ${token}`,
223
+ 'Content-Type': 'application/json',
224
+ },
225
+ });
226
+ comments = response.data.values;
227
+ } catch (e) {
228
+ console.error('Error while attempting to retrieve comments on Bitbucket Pull Request:\n', e);
229
+ }
230
+
231
+ if (!comments.length) return;
232
+
233
+ for (const comment of comments) {
234
+ // If comment was left by the same workflow
235
+ if (comment.content.raw.includes(hiddenCommentData)) {
236
+ try {
237
+ // Delete previous comment
238
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
239
+ await axiosInstance.delete(deleteCommentURL, {
240
+ headers: {
241
+ Authorization: `Bearer ${token}`,
242
+ 'Content-Type': 'application/json',
243
+ },
244
+ });
245
+ } catch (e) {
246
+ console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
247
+ }
248
+ // Pass next env var if need to clear all previous reports;
249
+ // only the last one is removed by default
250
+ if (!process.env.BITBUCKET_REMOVE_ALL_OUTDATED_REPORTS) break;
251
+ // TODO: in case of many reports should implement pagination
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,140 @@
1
+ import createDebugMessages from 'debug';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import {createObjectCsvWriter} from 'csv-writer';
5
+ import pc from 'picocolors';
6
+ import merge from 'lodash.merge';
7
+ import { isSameTest, getCurrentDateTime, ansiRegExp } from '../utils/utils.js';
8
+ import { CSV_HEADERS } from '../constants.js';
9
+
10
+ const debug = createDebugMessages('@testomatio/reporter:pipe:csv');
11
+ /**
12
+ * @typedef {import('../../types').Pipe} Pipe
13
+ * @typedef {import('../../types').TestData} TestData
14
+ * @class CsvPipe
15
+ * @implements {Pipe}
16
+ */
17
+ class CsvPipe {
18
+ constructor(params, store) {
19
+ this.store = store || {};
20
+ this.title = params.title || process.env.TESTOMATIO_TITLE;
21
+ this.results = [];
22
+
23
+ this.outputDir = 'export';
24
+ this.defaultReportName = 'report.csv';
25
+ this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
26
+ this.isEnabled = false;
27
+
28
+ if (this.csvFilename) {
29
+ const filenameParts = this.csvFilename.split('.');
30
+
31
+ if (filenameParts.length > 0) {
32
+ this.isEnabled = true;
33
+ const baseFilename = filenameParts[0];
34
+ const defaultOutputFile = path.resolve(process.cwd(), this.outputDir, this.defaultReportName);
35
+
36
+ const outputFile =
37
+ baseFilename === this.defaultReportName.split('.')[0] // = 'report'
38
+ ? defaultOutputFile
39
+ : path.resolve(process.cwd(), this.outputDir, `${getCurrentDateTime()}_${baseFilename}.csv`);
40
+
41
+ this.outputFile = outputFile;
42
+ }
43
+ }
44
+ }
45
+
46
+ // TODO: to using SET opts as argument => prepareRun(opts)
47
+ async prepareRun() {}
48
+
49
+ async createRun() {
50
+ // empty
51
+ }
52
+
53
+ updateRun() {}
54
+
55
+ /**
56
+ * Create a folder that will contain the exported files
57
+ */
58
+ checkExportDir() {
59
+ if (!fs.existsSync(this.outputDir)) {
60
+ return fs.mkdirSync(this.outputDir);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Save data to the csv file.
66
+ * @param {Object} data - data that will be added to the CSV file.
67
+ * Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
68
+ * @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
69
+ */
70
+ async saveToCsv(data, headers) {
71
+ debug('Data', data);
72
+ // First, we check whether the export directory exists: if yes - OK, no - create it.
73
+ this.checkExportDir();
74
+
75
+ if (!this.outputFile) {
76
+ console.log(pc.yellow(`⚠️ CSV file is not set, ignoring`));
77
+ return;
78
+ }
79
+
80
+ console.log(pc.yellow(`⏳ The test results will be added to the csv. It will take some time...`));
81
+
82
+ try {
83
+ // Create csv writer object
84
+ const writer = createObjectCsvWriter({
85
+ path: this.outputFile,
86
+ header: headers,
87
+ });
88
+ // Save csv file based on the current data
89
+ return await writer.writeRecords(data);
90
+ } catch (e) {
91
+ console.log('Unknown csv error: ', e);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Add test data to the result array for saving. As a result of this function, we get a result object to save.
97
+ * @param {Object} test - object which includes each test entry.
98
+ */
99
+ addTest(test) {
100
+ if (!this.isEnabled) return;
101
+
102
+ const index = this.results.findIndex(t => isSameTest(t, test));
103
+ // update if they were already added
104
+ if (index >= 0) {
105
+ this.results[index] = merge(this.results[index], test);
106
+ return;
107
+ }
108
+
109
+ const { suite_title, title, status, message, stack } = test;
110
+
111
+ this.results.push({
112
+ suite_title,
113
+ title,
114
+ status,
115
+ message: message.replace(ansiRegExp(), ''),
116
+ stack: stack.replace(ansiRegExp(), ''),
117
+ });
118
+ }
119
+
120
+ /**
121
+ * @param {{ tests?: TestData[] }} runParams
122
+ * @returns {Promise<void>}
123
+ */
124
+ async finishRun(runParams) {
125
+ if (!this.isEnabled) return;
126
+
127
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
128
+ // Save results based on the default headers
129
+ if (this.isEnabled) {
130
+ await this.saveToCsv(this.results, CSV_HEADERS);
131
+ console.log(pc.green(`🗃️ Recording completed! You can check the result in file = ${this.outputFile}`));
132
+ }
133
+ }
134
+
135
+ toString() {
136
+ return 'csv exporter';
137
+ }
138
+ }
139
+
140
+ export default CsvPipe;