@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.
@@ -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
+ | [![Testomat.io Report](https://avatars.githubusercontent.com/u/59105116?s=36&v=4)](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 += `![](${f})\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
- module.exports = function(params, opts) {
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 package = fs.readFileSync(packageJsonFile);
18
- const pipeDefs = package?.testomatio?.pipes || [];
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 pipeClass;
24
+ let PipeClass;
22
25
  try {
23
- pipeClass = require(pipeDef);
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 pipeClass(params, opts))
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
- ...extraPipes
43
+ new GitHubPipe(params, opts),
44
+ new GitLabPipe(params, opts),
45
+ new CsvPipe(params, opts),
46
+ ...extraPipes,
43
47
  ];
44
48
 
45
- console.log(APP_PREFIX, pipes.filter(p => p.isEnabled).map(p => p.toString()).join(', ') || "No pipe reporters enabled");
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;
@@ -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 { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_RUN } = process.env;
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
- isEnabled = false;
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
- if (process.env.DEBUG) {
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
- if (process.env.DEBUG) {
25
- console.log(APP_PREFIX, 'Testomatio Pipe: Enabled');
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.log(
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
- async createRun(runParams) {
46
+ /**
47
+ * @returns Promise<void>
48
+ */
49
+ async createRun() {
46
50
  if (!this.isEnabled) return;
47
51
 
48
- runParams.api_key = this.apiKey.trim();
49
- runParams.group_title = TESTOMATIO_RUNGROUP_TITLE;
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.log(
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
- async addTest(data) {
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
- try {
82
- return await this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
83
- maxContentLength: Infinity,
84
- maxBodyLength: Infinity,
85
- headers: {
86
- // Overwrite Axios's automatically set Content-Type
87
- 'Content-Type': 'application/json',
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 data = err.response.data || { message: '' };
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}) ${data.message}`,
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: params.statusEvent,
116
- status: params.status,
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
@@ -1,7 +1,9 @@
1
1
  const TestomatClient = require('./client');
2
2
  const TRConstants = require('./constants');
3
+ const TRArtifacts = require('./ArtifactStorage');
3
4
 
4
5
  module.exports = {
5
6
  TestomatClient,
6
7
  TRConstants,
8
+ TRArtifacts
7
9
  };
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.split(':')[0]))
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.includes(`vendor${ sep}`))
71
- .filter(l => !l.includes(`node_modules${ sep}`))
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) => t.title == test.title && t.suite_title == test.suite_title && Object.values(t.example) == Object.values(test.example) && t.test_id == test.test_id
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
+ }