@testomatio/reporter 1.2.2-beta → 1.2.2-beta.show-error.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.
Files changed (47) hide show
  1. package/README.md +61 -54
  2. package/lib/adapter/codecept.js +140 -61
  3. package/lib/adapter/cucumber/current.js +103 -60
  4. package/lib/adapter/cucumber/legacy.js +27 -12
  5. package/lib/adapter/cucumber.js +2 -2
  6. package/lib/adapter/cypress-plugin/index.js +52 -25
  7. package/lib/adapter/jasmine.js +1 -1
  8. package/lib/adapter/jest.js +49 -11
  9. package/lib/adapter/mocha.js +103 -51
  10. package/lib/adapter/playwright.js +100 -31
  11. package/lib/adapter/webdriver.js +1 -1
  12. package/lib/bin/reportXml.js +14 -13
  13. package/lib/bin/startTest.js +27 -6
  14. package/lib/client.js +193 -69
  15. package/lib/config.js +34 -0
  16. package/lib/constants.js +19 -7
  17. package/lib/data-storage.js +203 -0
  18. package/lib/fileUploader.js +128 -53
  19. package/lib/junit-adapter/adapter.js +0 -2
  20. package/lib/junit-adapter/csharp.js +3 -4
  21. package/lib/junit-adapter/index.js +3 -3
  22. package/lib/junit-adapter/java.js +35 -17
  23. package/lib/junit-adapter/javascript.js +1 -2
  24. package/lib/junit-adapter/python.js +12 -14
  25. package/lib/junit-adapter/ruby.js +1 -2
  26. package/lib/pipe/csv.js +5 -3
  27. package/lib/pipe/github.js +27 -39
  28. package/lib/pipe/gitlab.js +20 -24
  29. package/lib/pipe/html.js +317 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +207 -55
  32. package/lib/reporter-functions.js +46 -0
  33. package/lib/reporter.js +11 -9
  34. package/lib/services/artifacts.js +57 -0
  35. package/lib/services/index.js +13 -0
  36. package/lib/services/key-values.js +58 -0
  37. package/lib/services/logger.js +311 -0
  38. package/lib/template/template-draft.hbs +249 -0
  39. package/lib/template/testomatio.hbs +388 -0
  40. package/lib/utils/pipe_utils.js +128 -0
  41. package/lib/{util.js → utils/utils.js} +144 -12
  42. package/lib/xmlReader.js +211 -122
  43. package/package.json +18 -8
  44. package/lib/_ArtifactStorageOld.js +0 -142
  45. package/lib/artifactStorage.js +0 -25
  46. package/lib/dataStorage.js +0 -180
  47. package/lib/logger.js +0 -278
@@ -1,14 +1,18 @@
1
1
  const debug = require('debug')('@testomatio/reporter:pipe:testomatio');
2
2
  const chalk = require('chalk');
3
+ // Retry interceptor function
4
+ const axiosRetry = require('axios-retry');
5
+ // Default axios instance
3
6
  const axios = require('axios');
4
7
  const JsonCycle = require('json-cycle');
5
- const { APP_PREFIX, STATUS } = require('../constants');
6
- const { isValidUrl } = require('../util');
7
- const { resetConfig } = require('../fileUploader');
8
8
 
9
- const { TESTOMATIO_RUN } = process.env;
10
- if (TESTOMATIO_RUN) {
11
- process.env.runId = TESTOMATIO_RUN;
9
+ const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
10
+ const { isValidUrl, foundedTestLog } = require('../utils/utils');
11
+ const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
12
+ const config = require('../config');
13
+
14
+ if (process.env.TESTOMATIO_RUN) {
15
+ process.env.runId = process.env.TESTOMATIO_RUN;
12
16
  }
13
17
 
14
18
  /**
@@ -21,23 +25,53 @@ class TestomatioPipe {
21
25
  constructor(params, store) {
22
26
  this.isEnabled = false;
23
27
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
24
- this.apiKey = params.apiKey || process.env.TESTOMATIO;
28
+ this.apiKey = params.apiKey || config.TESTOMATIO;
25
29
  debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
26
30
  if (!this.apiKey) {
27
31
  return;
28
32
  }
29
33
  debug('Testomatio Pipe: Enabled');
34
+ this.parallel = params.parallel;
30
35
  this.store = store || {};
31
36
  this.title = params.title || process.env.TESTOMATIO_TITLE;
32
37
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
33
38
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
34
39
  this.env = process.env.TESTOMATIO_ENV;
35
- this.axios = axios.create();
40
+ this.label = process.env.TESTOMATIO_LABEL;
41
+ // Create a new instance of axios with a custom config
42
+ this.axios = axios.create({
43
+ baseURL: `${this.url.trim()}`,
44
+ timeout: AXIOS_TIMEOUT,
45
+ });
46
+ // Pass the axios instance to the retry function
47
+ axiosRetry(this.axios, {
48
+ retries: 3, // Number of retries (Defaults to 3)
49
+ shouldResetTimeout: true,
50
+ retryCondition: error => {
51
+ // Conditional check the error status code
52
+ switch (error.response.status) {
53
+ case 409:
54
+ case 429:
55
+ case 502:
56
+ case 503:
57
+ return true; // Retry request with response status code 409, 429, 502, 503
58
+ default:
59
+ return false; // Do not retry the others
60
+ }
61
+ },
62
+ retryDelay: retryCount => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
63
+ onRetry: retryCount => {
64
+ debug(`Retry attempt #${retryCount} failed. Retrying again...`);
65
+ },
66
+ });
67
+
36
68
  this.isEnabled = true;
37
69
  // do not finish this run (for parallel testing)
38
70
  this.proceed = process.env.TESTOMATIO_PROCEED;
71
+ this.jiraId = process.env.TESTOMATIO_JIRA_ID;
39
72
  this.runId = params.runId || process.env.runId;
40
- this.createNewTests = !!process.env.TESTOMATIO_CREATE;
73
+ this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
74
+ this.hasUnmatchedTests = false;
41
75
 
42
76
  if (!isValidUrl(this.url.trim())) {
43
77
  this.isEnabled = false;
@@ -45,53 +79,122 @@ class TestomatioPipe {
45
79
  }
46
80
  }
47
81
 
82
+ /**
83
+ * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
84
+ * @param {Object} opts - The options for preparing the test grepList.
85
+ * @returns {Promise<string[]>} - An array containing the retrieved
86
+ * test grepList, or an empty array if no tests are found or the request is disabled.
87
+ * @throws {Error} - Throws an error if there was a problem while making the request.
88
+ */
89
+ async prepareRun(opts) {
90
+ if (!this.isEnabled) return [];
91
+
92
+ const { type, id } = parseFilterParams(opts);
93
+
94
+ try {
95
+ const q = generateFilterRequestParams({
96
+ type,
97
+ id,
98
+ apiKey: this.apiKey.trim(),
99
+ });
100
+
101
+ if (!q) {
102
+ return;
103
+ }
104
+
105
+ const resp = await this.axios.get('/api/test_grep', q);
106
+ const { data } = resp;
107
+
108
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
109
+ foundedTestLog(APP_PREFIX, data.tests);
110
+ return data.tests;
111
+ }
112
+
113
+ console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
114
+ } catch (err) {
115
+ console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
116
+ }
117
+ }
118
+
48
119
  /**
49
120
  * @returns Promise<void>
50
121
  */
51
122
  async createRun() {
123
+ debug('Creating run...');
52
124
  if (!this.isEnabled) return;
53
125
 
126
+ let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
127
+
128
+ // GitHub Actions Url
129
+ if (!buildUrl && process.env.GITHUB_RUN_ID) {
130
+ // eslint-disable-next-line max-len
131
+ buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
132
+ }
133
+
134
+ // Azure DevOps Url
135
+ if (!buildUrl && process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
136
+ const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
137
+ const project = process.env.SYSTEM_TEAMPROJECT;
138
+ const buildId = process.env.BUILD_BUILDID;
139
+ buildUrl = `${collectionUri}/${project}/_build/results?buildId=${buildId}`;
140
+ }
141
+
142
+ if (buildUrl && !buildUrl.startsWith('http')) buildUrl = undefined;
143
+
144
+ const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
145
+
54
146
  const runParams = Object.fromEntries(
55
147
  Object.entries({
148
+ ci_build_url: buildUrl,
149
+ parallel: this.parallel,
56
150
  api_key: this.apiKey.trim(),
57
151
  group_title: this.groupTitle,
152
+ access_event: accessEvent,
153
+ jira_id: this.jiraId,
58
154
  env: this.env,
59
155
  title: this.title,
156
+ label: this.label,
60
157
  shared_run: this.sharedRun,
61
- }).filter(([, value]) => !!value)
158
+ }).filter(([, value]) => !!value),
62
159
  );
160
+ debug('Run params', JSON.stringify(runParams, null, 2));
63
161
 
64
162
  if (this.runId) {
65
- const resp = await this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams);
163
+ debug(`Run with id ${this.runId} already created, updating...`);
164
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
66
165
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
67
166
  return;
68
167
  }
69
168
 
70
169
  try {
71
- const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
170
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
72
171
  maxContentLength: Infinity,
73
172
  maxBodyLength: Infinity,
74
173
  });
174
+
75
175
  this.runId = resp.data.uid;
76
176
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
177
+ this.runPublicUrl = resp.data.public_url;
178
+
77
179
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
180
+
78
181
  this.store.runUrl = this.runUrl;
182
+ this.store.runPublicUrl = this.runPublicUrl;
79
183
  this.store.runId = this.runId;
80
184
  console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
81
185
  process.env.runId = this.runId;
186
+ debug('Run created', this.runId);
82
187
  } catch (err) {
83
188
  console.error(
84
189
  APP_PREFIX,
85
- 'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
190
+ 'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
191
+ err?.response?.statusText || err?.status || err.message,
86
192
  );
193
+ printCreateIssue(err);
87
194
  }
195
+ debug('"createRun" function finished');
88
196
  }
89
197
 
90
- /**
91
- *
92
- * @param testData data
93
- * @returns
94
- */
95
198
  addTest(data) {
96
199
  if (!this.isEnabled) return;
97
200
  if (!this.runId) return;
@@ -99,37 +202,51 @@ class TestomatioPipe {
99
202
  data.create = this.createNewTests;
100
203
  const json = JsonCycle.stringify(data);
101
204
 
102
- return this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
103
- maxContentLength: Infinity,
104
- maxBodyLength: Infinity,
105
- headers: {
106
- // Overwrite Axios's automatically set Content-Type
107
- 'Content-Type': 'application/json',
108
- },
109
- })
110
- .catch((err) => {
111
- if (err.response) {
112
- if (err.response.status >= 400) {
113
- const responseData = err.response.data || { message: '' };
205
+ debug('Adding test', json);
206
+
207
+ return this.axios
208
+ .post(`/api/reporter/${this.runId}/testrun`, json, {
209
+ maxContentLength: Infinity,
210
+ maxBodyLength: Infinity,
211
+ headers: {
212
+ // Overwrite Axios's automatically set Content-Type
213
+ 'Content-Type': 'application/json',
214
+ },
215
+ })
216
+ .catch(err => {
217
+ if (err.response) {
218
+ if (err.response.status >= 400) {
219
+ const responseData = err.response.data || { message: '' };
220
+ console.log(
221
+ APP_PREFIX,
222
+ chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
223
+ chalk.grey(data?.title || ''),
224
+ );
225
+ if (err.response.data.message.includes('could not be matched')) {
226
+ this.hasUnmatchedTests = true;
227
+ }
228
+ return;
229
+ }
114
230
  console.log(
115
231
  APP_PREFIX,
116
- chalk.blue(this.title),
117
- `Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
232
+ chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
233
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
118
234
  );
119
- return;
235
+ printCreateIssue(err);
236
+ } else {
237
+ console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
238
+
239
+
120
240
  }
121
- console.log(APP_PREFIX, chalk.blue(this.title), `Report couldn't be processed: ${err.response.data.message}`);
122
- } else {
123
- console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
124
- }
125
- });
241
+ });
126
242
  }
127
243
 
128
244
  /**
129
- * @param {import('../../types').RunData} params
130
- * @returns
245
+ * @param {import('../../types').RunData} params
246
+ * @returns
131
247
  */
132
248
  async finishRun(params) {
249
+ debug('Finishing run...');
133
250
  if (!this.isEnabled) return;
134
251
 
135
252
  const { status, parallel } = params;
@@ -143,7 +260,7 @@ class TestomatioPipe {
143
260
 
144
261
  try {
145
262
  if (this.runId && !this.proceed) {
146
- await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
263
+ await this.axios.put(`/api/reporter/${this.runId}`, {
147
264
  api_key: this.apiKey,
148
265
  status_event,
149
266
  tests: params.tests,
@@ -151,15 +268,45 @@ class TestomatioPipe {
151
268
  if (this.runUrl) {
152
269
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
153
270
  }
271
+ if (this.runPublicUrl) {
272
+ console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
273
+ }
154
274
  }
155
275
  if (this.runUrl && this.proceed) {
156
276
  const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
157
277
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
158
278
  console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
159
279
  }
280
+ if (this.hasUnmatchedTests) {
281
+ console.log('');
282
+ // eslint-disable-next-line max-len
283
+ console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
284
+ // eslint-disable-next-line max-len
285
+ console.log(
286
+ APP_PREFIX,
287
+ `If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
288
+ );
289
+ // eslint-disable-next-line max-len
290
+ console.log(
291
+ APP_PREFIX,
292
+ `But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
293
+ );
294
+ console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
295
+ console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
296
+ console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
297
+ console.log(APP_PREFIX, 'or for Cucumber:');
298
+ // eslint-disable-next-line max-len
299
+ console.log(
300
+ APP_PREFIX,
301
+ chalk.bold('npx check-cucumber ... --update-ids'),
302
+ 'See: https://bit.ly/bdd-update-ids',
303
+ );
304
+ }
160
305
  } catch (err) {
161
306
  console.log(APP_PREFIX, 'Error updating status, skipping...', err);
307
+ printCreateIssue(err);
162
308
  }
309
+ debug('Run finished');
163
310
  }
164
311
 
165
312
  toString() {
@@ -167,19 +314,24 @@ class TestomatioPipe {
167
314
  }
168
315
  }
169
316
 
170
- module.exports = TestomatioPipe;
171
-
172
-
173
- function setS3Credentials(artifacts) {
174
- if (!Object.keys(artifacts).length) return;
317
+ let registeredErrorHints = false;
318
+ function printCreateIssue(err) {
319
+ if (registeredErrorHints) return;
320
+ registeredErrorHints = true;
321
+ process.on('beforeExit', () => {
322
+ console.log(APP_PREFIX, 'There was error reporting to Testomat.io:');
323
+ console.log(APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/app/issues/new');
324
+ console.log(APP_PREFIX, 'Provide this information:');
325
+ console.log(APP_PREFIX, 'Error:', err.message || err.code);
326
+ if (!err.config) return;
175
327
 
176
- console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
177
-
178
- if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
179
- if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
180
- if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
181
- if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
182
- if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
183
- if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
184
- resetConfig();
328
+ const time = new Date().toUTCString();
329
+ const { data, url, baseURL, method } = err?.config || {}
330
+ console.log('```js')
331
+ console.log({ data, url, baseURL, method, time });
332
+ console.log('```')
333
+ console.log();
334
+ });
185
335
  }
336
+
337
+ module.exports = TestomatioPipe;
@@ -0,0 +1,46 @@
1
+ const { services } = require('./services');
2
+ const { initPlaywrightForStorage } = require('./adapter/playwright');
3
+
4
+ if (process.env.PLAYWRIGHT_TEST_BASE_URL) {
5
+ initPlaywrightForStorage();
6
+ }
7
+
8
+ /**
9
+ * Stores path to file as artifact and uploads it to the S3 storage
10
+ * @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
11
+ */
12
+ function saveArtifact(data, context = null) {
13
+ if (!data) return;
14
+ services.artifacts.put(data, context);
15
+ }
16
+
17
+ /**
18
+ * Attach log message(s) to the test report
19
+ * @param {...any} args
20
+ */
21
+ function logMessage(...args) {
22
+ services.logger._templateLiteralLog(...args);
23
+ }
24
+
25
+ /**
26
+ * Similar to "log" function but marks message in report as a step
27
+ * @param {*} message
28
+ */
29
+ function addStep(message) {
30
+ services.logger.step(message);
31
+ }
32
+
33
+ /**
34
+ * Add key-value pair(s) to the test report
35
+ * @param {*} keyValue
36
+ */
37
+ function setKeyValue(keyValue) {
38
+ services.keyValues.put(keyValue);
39
+ }
40
+
41
+ module.exports = {
42
+ artifact: saveArtifact,
43
+ log: logMessage,
44
+ step: addStep,
45
+ keyValue: setKeyValue,
46
+ };
package/lib/reporter.js CHANGED
@@ -1,17 +1,19 @@
1
- const logger = require('./logger');
2
1
  const TestomatClient = require('./client');
3
2
  const TRConstants = require('./constants');
4
- const TRArtifacts = require('./_ArtifactStorageOld');
3
+ const { services } = require('./services');
5
4
 
6
- const log = logger._log.bind(logger);
7
- const step = logger.step.bind(logger);
5
+ const reporterFunctions = require('./reporter-functions');
8
6
 
9
7
  module.exports = {
10
- logger,
11
- log,
12
- step,
8
+ // TODO: deprecate in future; use log or testomat.log
9
+ testomatioLogger: services.logger,
10
+
11
+ artifact: reporterFunctions.artifact,
12
+ log: reporterFunctions.log,
13
+ logger: services.logger,
14
+ meta: reporterFunctions.keyValue,
15
+ step: reporterFunctions.step,
16
+
13
17
  TestomatClient,
14
18
  TRConstants,
15
- TRArtifacts,
16
- addArtifact: TRArtifacts.artifact,
17
19
  };
@@ -0,0 +1,57 @@
1
+ const debug = require('debug')('@testomatio/reporter:services-artifacts');
2
+ const { dataStorage } = require('../data-storage');
3
+
4
+ /**
5
+ * Artifact storage is supposed to store file paths
6
+ */
7
+ class ArtifactStorage {
8
+ static #instance;
9
+
10
+ /**
11
+ * Singleton
12
+ * @returns {ArtifactStorage}
13
+ */
14
+ static getInstance() {
15
+ if (!this.#instance) {
16
+ this.#instance = new ArtifactStorage();
17
+ }
18
+ return this.#instance;
19
+ }
20
+
21
+ /**
22
+ * Stores path to file as artifact and uploads it to the S3 storage
23
+ * @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
24
+ * @param {*} context testId or test title
25
+ */
26
+ put(data, context = null) {
27
+ if (!data) return;
28
+ debug('Save artifact:', data);
29
+ dataStorage.putData('artifact', data, context);
30
+ }
31
+
32
+ /**
33
+ * Returns list of artifacts to upload
34
+ * @param {*} context testId or test context from test runner
35
+ * @returns {(string | {path: string, type: string, name: string})[]}
36
+ */
37
+ get(context) {
38
+ let artifacts = dataStorage.getData('artifact', context);
39
+ if (!artifacts || !artifacts.length) return [];
40
+
41
+ artifacts = artifacts.map(artifactData => {
42
+ // artifact could be an object ({type, path, name} props) or string (just path)
43
+ let artifact;
44
+ try {
45
+ artifact = JSON.parse(artifactData);
46
+ } catch (e) {
47
+ artifact = artifactData;
48
+ }
49
+ return artifact;
50
+ });
51
+ artifacts = artifacts.filter(artifact => !!artifact);
52
+ debug(`Artifacts for test ${context}:`, artifacts);
53
+ return artifacts.length ? artifacts : [];
54
+ }
55
+ }
56
+
57
+ module.exports.artifactStorage = ArtifactStorage.getInstance();
@@ -0,0 +1,13 @@
1
+ const { logger } = require('./logger');
2
+ const { artifactStorage } = require('./artifacts');
3
+ const { keyValueStorage } = require('./key-values');
4
+ const { dataStorage } = require('../data-storage');
5
+
6
+ module.exports.services = {
7
+ logger,
8
+ artifacts: artifactStorage,
9
+ keyValues: keyValueStorage,
10
+ setContext: context => {
11
+ dataStorage.setContext(context);
12
+ },
13
+ };
@@ -0,0 +1,58 @@
1
+ const debug = require('debug')('@testomatio/reporter:services-key-value');
2
+ const { dataStorage } = require('../data-storage');
3
+
4
+ class KeyValueStorage {
5
+ static #instance;
6
+
7
+ /**
8
+ *
9
+ * @returns {KeyValueStorage}
10
+ */
11
+ static getInstance() {
12
+ if (!this.#instance) {
13
+ this.#instance = new KeyValueStorage();
14
+ }
15
+ return this.#instance;
16
+ }
17
+
18
+ /**
19
+ * Stores key-value pair and passes it to reporter
20
+ * @param {{key: string}} keyValue - key-value pair(s) as object
21
+ * @param {*} context - full test title
22
+ */
23
+ put(keyValue, context = null) {
24
+ if (!keyValue) return;
25
+ dataStorage.putData('keyvalue', keyValue, context);
26
+ }
27
+
28
+ #isKeyValueObject(smth) {
29
+ return smth && typeof smth === 'object' && !Array.isArray(smth) && smth !== null;
30
+ }
31
+
32
+ /**
33
+ * Returns key-values pairs for the test as object
34
+ * @param {*} context testId or test context from test runner
35
+ * @returns {{[key: string]: string} | {}} key-values pairs as object, e.g. {priority: 'high', browser: 'chrome'}
36
+ */
37
+ get(context = null) {
38
+ const keyValuesList = dataStorage.getData('keyvalue', context);
39
+ if (!keyValuesList || !keyValuesList?.length) return {};
40
+
41
+ const keyValues = {};
42
+ for (const keyValue of keyValuesList) {
43
+ if (this.#isKeyValueObject(keyValue)) {
44
+ Object.assign(keyValues, keyValue);
45
+ } else if (typeof keyValue === 'string') {
46
+ try {
47
+ Object.assign(keyValues, JSON.parse(keyValue));
48
+ } catch (e) {
49
+ debug(`Error parsing key-values for test ${context}`, keyValue);
50
+ }
51
+ }
52
+ }
53
+
54
+ return keyValues;
55
+ }
56
+ }
57
+
58
+ module.exports.keyValueStorage = KeyValueStorage.getInstance();