@testomatio/reporter 0.8.0-beta.15 → 0.8.0-beta.20

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
@@ -208,6 +208,24 @@ Run the following command from you project folder:
208
208
  TESTOMATIO={API_KEY} npx start-test-run -c 'npx wdio wdio.conf.js'
209
209
  ```
210
210
 
211
+ ## Pipes
212
+ Pipes allow you to get report inside different systems (e.g. Pull request comment, database etc)
213
+ For now next pipes available:
214
+
215
+ ### Github
216
+ This pipe adds comment with run report to GitHub Pull Request.
217
+
218
+ To use it:
219
+ 1. run your tests using github actions in Pull Request
220
+ 2. pass `GH_PAT` (GitHub Personal Access Token) as environment variable.
221
+
222
+ ### GitLab
223
+ This pipe adds comment with run report to GitLab Merge Request.
224
+
225
+ To use it:
226
+ 1. run your tests in Merge Request (pipeline trigger should be `merge_request`)
227
+ 2. pass `GITLAB_PAT` (GitLab Personal Access Token) as environment variable.
228
+
211
229
  ## JUnit Reports
212
230
 
213
231
  > **Notice** JUnit reports are supported since 0.6.0
@@ -408,6 +426,23 @@ Add environments to run by providing `TESTOMATIO_ENV` as comma seperated values:
408
426
  TESTOMATIO={API_KEY} TESTOMATIO_ENV="Windows, Chrome" <actual run command>
409
427
  ```
410
428
 
429
+ ### Save test results to .csv file
430
+ Add an env to run by specifying the `TESTOMATIO_CSV_FILENAME` variable.
431
+
432
+ 1) using default report name:
433
+
434
+ ```bash
435
+ TESTOMATIO={API_KEY} TESTOMATIO_CSV_FILENAME="report.csv" <actual run command>
436
+ ```
437
+
438
+ 2) using unique report name:
439
+
440
+ ```bash
441
+ TESTOMATIO={API_KEY} TESTOMATIO_CSV_FILENAME="test.csv" <actual run command>
442
+ ```
443
+ _It's create a new /export folder with csv files_
444
+
445
+
411
446
  ### Attaching Test Artifacts
412
447
 
413
448
  To save a test artifacts (screenshots and videos) of a failed test use S3 storage.
package/lib/constants.js CHANGED
@@ -2,10 +2,19 @@ const chalk = require('chalk');
2
2
 
3
3
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
4
4
 
5
+ const CSV_HEADERS = [
6
+ { id: 'suite_title', title: 'Suite_title' },
7
+ { id: 'title', title: 'Title' },
8
+ { id: 'status', title: 'Status' },
9
+ { id: 'message', title: 'Message' },
10
+ { id: 'stack', title: 'Stack' },
11
+ ];
12
+
5
13
  module.exports = Object.freeze({
6
14
  PASSED: 'passed',
7
15
  FAILED: 'failed',
8
16
  SKIPPED: 'skipped',
9
17
  FINISHED: 'finished',
10
18
  APP_PREFIX,
19
+ CSV_HEADERS
11
20
  });
@@ -0,0 +1,111 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+ const csvWriter = require('csv-writer');
4
+ const chalk = require('chalk');
5
+ const merge = require('lodash.merge');
6
+ const { isSameTest, getCurrentDateTime } = require('../util');
7
+ const { CSV_HEADERS } = require('../constants');
8
+
9
+ class CsvPipe {
10
+
11
+ constructor(params, store = {}) {
12
+ this.store = store;
13
+ this.title = params.title || process.env.TESTOMATIO_TITLE;
14
+ this.isEnabled = true;
15
+ this.results = [];
16
+
17
+ this.outputDir = "export";
18
+ this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
19
+ this.isCsvSave = false;
20
+
21
+ if (this.csvFilename !== undefined && this.csvFilename.split(".").length > 0 ) {
22
+ this.isCsvSave = true;
23
+
24
+ if(this.csvFilename.split(".")[0] === "report") {
25
+ this.outputFile = path.resolve(process.cwd(), this.outputDir, "report.csv");
26
+ }
27
+ else {
28
+ this.outputFile = path.resolve(process.cwd(), this.outputDir, getCurrentDateTime() + "_" + this.csvFilename.split(".")[0] + ".csv");
29
+ }
30
+ }
31
+ }
32
+
33
+ async createRun() {}
34
+
35
+ updateRun() {}
36
+
37
+ /**
38
+ * Create a folder that will contain the exported files
39
+ */
40
+ checkExportDir() {
41
+ if (!fs.existsSync(this.outputDir)) {
42
+ return fs.mkdirSync(this.outputDir);
43
+ }
44
+ }
45
+ /**
46
+ * Save data to the csv file.
47
+ * @param {Object} data - data that will be added to the CSV file. Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
48
+ * @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
49
+ */
50
+ async saveToCsv(data, headers) {
51
+ // First, we check whether the export directory exists: if yes - OK, no - create it.
52
+ this.checkExportDir();
53
+
54
+ console.log(chalk.yellow(`The test results will be added to the csv. It will take some time...`));
55
+ //Create csv writer object
56
+ const writer = csvWriter.createObjectCsvWriter({
57
+ path: this.outputFile,
58
+ header: headers
59
+ });
60
+ //Save csv file based on the current data
61
+ try {
62
+ await writer.writeRecords(data);
63
+ console.log(chalk.green(`Recording completed! You can check the result in file = ${this.outputFile}`));
64
+ } catch(e) {
65
+ console.log('Unknown error: ', e);
66
+ }
67
+ }
68
+ /**
69
+ * Add test data to the result array for saving. As a result of this function, we get a result object to save.
70
+ * @param {Object} test - object which includes each test entry.
71
+ */
72
+ addTest(test) {
73
+ if (!this.isEnabled) return;
74
+
75
+ const index = this.results.findIndex(t => isSameTest(t, test));
76
+ // update if they were already added
77
+ if (index >= 0) {
78
+ this.results[index] = merge(this.results[index], test);
79
+ return;
80
+ }
81
+
82
+ const {suite_title, title, status, message, stack} = test;
83
+
84
+ this.results.push({
85
+ suite_title,
86
+ title,
87
+ status,
88
+ message,
89
+ stack
90
+ });
91
+ }
92
+ /**
93
+ * Save short tests data to the csv file.
94
+ * @param {Object} runParams - run params.
95
+ */
96
+ async finishRun(runParams) {
97
+ if (!this.isEnabled) return;
98
+
99
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
100
+ //Save results based on the default headers
101
+ if(this.isCsvSave) {
102
+ this.saveToCsv(this.results, CSV_HEADERS);
103
+ }
104
+ }
105
+
106
+ toString() {
107
+ return 'csv exporter';
108
+ }
109
+ }
110
+
111
+ module.exports = CsvPipe;
@@ -122,11 +122,11 @@ class GitHubPipe {
122
122
  if (failures.length > 20) {
123
123
  body += "\n> Notice\n> Only first 20 failures shown*"
124
124
  }
125
- body += "\n\n</details>\n";
125
+ body += "\n\n</details>";
126
126
  }
127
127
 
128
128
  if (this.tests.length > 0) {
129
- body += "<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n"
129
+ body += "\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n"
130
130
  body += this.tests.sort((a, b) => b?.run_time - a?.run_time).slice(0, 5).map(t => {
131
131
  return `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`
132
132
  }).join('\n')
@@ -0,0 +1,192 @@
1
+ const { ansiRegExp, isSameTest } = require('../util');
2
+ const { APP_PREFIX } = require('../constants');
3
+ const chalk = require('chalk');
4
+ const got = require('got');
5
+ const humanizeDuration = require('humanize-duration');
6
+ const merge = require('lodash.merge');
7
+ const path = require('path');
8
+
9
+ //! GITLAB_PAT environment variable is required for this functionality to work and your pipeline trigger should be merge_request
10
+
11
+ class GitLabPipe {
12
+ isEnabled = false;
13
+
14
+ constructor(params, store = {}) {
15
+ this.ENV = process.env;
16
+ this.store = store;
17
+ this.tests = [];
18
+ // GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
19
+ this.token = params.GITLAB_PAT || this.ENV.GITLAB_PAT;
20
+
21
+ if (this.ENV.DEBUG) {
22
+ console.log(
23
+ APP_PREFIX,
24
+ chalk.yellow('GitLab Pipe:'),
25
+ this.token ? 'TOKEN passed' : '*no token*',
26
+ `Project id: ${this.ENV.CI_PROJECT_ID}, MR id: ${this.ENV.CI_MERGE_REQUEST_IID}`,
27
+ );
28
+ }
29
+
30
+ if (!this.ENV.CI_PROJECT_ID || !this.ENV.CI_MERGE_REQUEST_IID) {
31
+ console.warn(
32
+ `CI pipeline should be run in Merge Request to have ability to add the report comment.`,
33
+ );
34
+ return;
35
+ }
36
+
37
+ if (!this.token) {
38
+ console.warn(chalk.grey(`Hint: GitLab CI variables are unavailable for unprotected branches by default.`));
39
+ return;
40
+ }
41
+
42
+ this.isEnabled = true;
43
+
44
+ if (this.ENV.DEBUG) {
45
+ console.log(APP_PREFIX, chalk.yellow('GitLab Pipe: Enabled'));
46
+ }
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 = `
76
+
77
+ | [![Testomat.io Report](https://avatars.githubusercontent.com/u/59105116?s=36&v=4)](https://testomat.io) | ${statusEmoji(
78
+ runParams.status,
79
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
80
+ | --- | --- |
81
+ | Tests | ✔️ **${this.tests.length}** tests run |
82
+ | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
83
+ 'passed',
84
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
85
+ | Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
86
+ `;
87
+
88
+ if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
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 +=
100
+ '> ' +
101
+ t.message
102
+ .replace(/[^\x20-\x7E]/g, '')
103
+ .replace(ansiRegExp(), '')
104
+ .trim() +
105
+ '\n';
106
+ if (t.stack) text += '```diff\n' + t.stack.replace(ansiRegExp(), '').trim() + '\n```\n';
107
+
108
+ if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
109
+ t.artifacts
110
+ .filter(f => !!f)
111
+ .filter(f => f.endsWith('.png'))
112
+ .forEach(f => {
113
+ if (f.endsWith('.png')) return (text += `![](${f})\n`);
114
+ return (text += `[📄 ${path.basename(f)}](${f})\n`);
115
+ });
116
+ }
117
+
118
+ text += '\n---\n';
119
+
120
+ return text;
121
+ });
122
+
123
+ let body = summary;
124
+
125
+ if (failures.length) {
126
+ body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
127
+ if (failures.length > 20) {
128
+ body += '\n> Notice\n> Only first 20 failures shown*';
129
+ }
130
+ body += '\n\n</details>';
131
+ }
132
+
133
+ if (this.tests.length > 0) {
134
+ body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
135
+ body += this.tests
136
+ .sort((a, b) => b?.run_time - a?.run_time)
137
+ .slice(0, 5)
138
+ .map(t => {
139
+ return `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`;
140
+ })
141
+ .join('\n');
142
+ body += '\n</details>';
143
+ }
144
+
145
+ try {
146
+ const addCommentRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes?access_token=${this.token}`;
147
+ console.info(chalk.grey(`Adding comment via url: ${addCommentRequestURL}`));
148
+ const options = {
149
+ json: {
150
+ body,
151
+ },
152
+ };
153
+
154
+ const addCommentResponse = await got.post(addCommentRequestURL, options);
155
+ const commentID = JSON.parse(addCommentResponse.body).id;
156
+ const commentURL = `${this.ENV.CI_PROJECT_URL}/-/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}#note_${commentID}`;
157
+
158
+ console.log(APP_PREFIX, chalk.yellow('GitLab'), `Report created: ${chalk.magenta(commentURL)}`);
159
+ } catch (err) {
160
+ console.log(
161
+ APP_PREFIX,
162
+ chalk.yellow('GitLab'),
163
+ `Couldn't create GitLab report\n${err}.
164
+ Reqest url: ${addCommentRequestURL}
165
+ Request data: ${JSON.stringify(options)}`,
166
+ );
167
+ }
168
+ }
169
+
170
+ toString() {
171
+ return 'GitLab Reporter';
172
+ }
173
+
174
+ updateRun() {}
175
+ }
176
+
177
+ function statusEmoji(status) {
178
+ if (status === 'passed') return '🟢';
179
+ if (status === 'failed') return '🔴';
180
+ if (status === 'skipped') return '🟡';
181
+ return '';
182
+ }
183
+
184
+ function fullName(t) {
185
+ let line = '';
186
+ if (t.suite_title) line = `${t.suite_title}: `;
187
+ line += `**${t.title}**`;
188
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
189
+ return line;
190
+ }
191
+
192
+ module.exports = GitLabPipe;
package/lib/pipe/index.js CHANGED
@@ -4,6 +4,8 @@ const chalk = require('chalk');
4
4
  const { APP_PREFIX } = require('../constants');
5
5
  const TestomatioPipe = require('./testomatio');
6
6
  const GitHubPipe = require('./github');
7
+ const GitLabPipe = require('./gitlab');
8
+ const CsvPipe = require('./csv');
7
9
 
8
10
  module.exports = function(params, opts) {
9
11
  const extraPipes = [];
@@ -39,7 +41,9 @@ module.exports = function(params, opts) {
39
41
 
40
42
  const pipes = [
41
43
  new TestomatioPipe(params, opts),
42
- new GitHubPipe(params, opts),
44
+ new GitHubPipe(params, opts),
45
+ new GitLabPipe(params, opts),
46
+ new CsvPipe(params, opts),
43
47
  ...extraPipes
44
48
  ];
45
49
 
package/lib/util.js CHANGED
@@ -146,6 +146,13 @@ const isSameTest = (test, t) => {
146
146
  && t.test_id == test.test_id
147
147
  };
148
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
+
149
156
  module.exports = {
150
157
  parseTest,
151
158
  parseSuite,
@@ -155,4 +162,5 @@ module.exports = {
155
162
  fetchSourceCode,
156
163
  fetchSourceCodeFromStackTrace,
157
164
  fetchFilesFromStackTrace,
165
+ getCurrentDateTime
158
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "0.8.0-beta.15",
3
+ "version": "0.8.0-beta.20",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "repository": "git@github.com:testomatio/reporter.git",
@@ -16,12 +16,14 @@
16
16
  "dotenv": "^16.0.1",
17
17
  "fast-xml-parser": "^4.0.8",
18
18
  "glob": "^8.0.3",
19
+ "got": "^11.8.3",
19
20
  "has-flag": "^5.0.1",
20
21
  "humanize-duration": "^3.27.3",
21
22
  "is-valid-path": "^0.1.1",
22
23
  "json-cycle": "^1.3.0",
23
24
  "lodash.memoize": "^4.1.2",
24
- "lodash.merge": "^4.6.2"
25
+ "lodash.merge": "^4.6.2",
26
+ "csv-writer": "^1.6.0"
25
27
  },
26
28
  "files": [
27
29
  "bin",
@@ -29,6 +31,7 @@
29
31
  "testcafe"
30
32
  ],
31
33
  "scripts": {
34
+ "clear-exportdir": "rm -rf export/",
32
35
  "pretty": "prettier --write .",
33
36
  "lint": "eslint lib",
34
37
  "lint:fix": "eslint lib --fix",
@@ -36,6 +39,7 @@
36
39
  "init": "cd ./tests/adapter/examples/cucumber && npm i",
37
40
  "test:unit": "mocha tests",
38
41
  "test:adapter": "mocha './tests/adapter/index.test.js'",
42
+ "test:pipes": "mocha './tests/pipes/*_test.js'",
39
43
  "test:adapter:jest:example": "jest './tests/adapter/examples/jest/index.test.js' --config='./tests/adapter/examples/jest/jest.config.js'",
40
44
  "test:adapter:mocha:example": "mocha './tests/adapter/examples/mocha/index.test.js' --config='./tests/adapter/examples/mocha/mocha.config.js'",
41
45
  "test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",