@testomatio/reporter 1.2.3-beta → 1.2.3-beta-batch-upload

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 (48) hide show
  1. package/README.md +61 -54
  2. package/lib/adapter/codecept.js +96 -35
  3. package/lib/adapter/cucumber/current.js +23 -16
  4. package/lib/adapter/cucumber/legacy.js +14 -13
  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 -16
  9. package/lib/adapter/mocha.js +103 -51
  10. package/lib/adapter/playwright.js +80 -27
  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 +192 -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 +220 -62
  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 +215 -122
  43. package/package.json +18 -7
  44. package/lib/_ArtifactStorageOld.js +0 -142
  45. package/lib/artifactStorage.js +0 -25
  46. package/lib/dataStorage.js +0 -241
  47. package/lib/helpers.js +0 -34
  48. package/lib/logger.js +0 -293
@@ -1,14 +1,17 @@
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
- const JsonCycle = require('json-cycle');
5
- const { APP_PREFIX, STATUS } = require('../constants');
6
- const { isValidUrl } = require('../util');
7
- const { resetConfig } = require('../fileUploader');
8
-
9
- const { TESTOMATIO_RUN } = process.env;
10
- if (TESTOMATIO_RUN) {
11
- process.env.runId = TESTOMATIO_RUN;
7
+
8
+ const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
9
+ const { isValidUrl, foundedTestLog } = require('../utils/utils');
10
+ const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
11
+ const config = require('../config');
12
+
13
+ if (process.env.TESTOMATIO_RUN) {
14
+ process.env.runId = process.env.TESTOMATIO_RUN;
12
15
  }
13
16
 
14
17
  /**
@@ -18,26 +21,62 @@ if (TESTOMATIO_RUN) {
18
21
  * @implements {Pipe}
19
22
  */
20
23
  class TestomatioPipe {
24
+ batch = {
25
+ tests: [],
26
+ intervalFunction: null,
27
+ intervalTime: 5000, // how often tests are sent
28
+ };
29
+
21
30
  constructor(params, store) {
22
31
  this.isEnabled = false;
23
32
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
24
- this.apiKey = params.apiKey || process.env.TESTOMATIO;
33
+ this.apiKey = params.apiKey || config.TESTOMATIO;
25
34
  debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
26
35
  if (!this.apiKey) {
27
36
  return;
28
37
  }
29
38
  debug('Testomatio Pipe: Enabled');
39
+ this.parallel = params.parallel;
30
40
  this.store = store || {};
31
41
  this.title = params.title || process.env.TESTOMATIO_TITLE;
32
42
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
33
43
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
34
44
  this.env = process.env.TESTOMATIO_ENV;
35
- this.axios = axios.create();
45
+ this.label = process.env.TESTOMATIO_LABEL;
46
+ // Create a new instance of axios with a custom config
47
+ this.axios = axios.create({
48
+ baseURL: `${this.url.trim()}`,
49
+ timeout: AXIOS_TIMEOUT,
50
+ });
51
+ // Pass the axios instance to the retry function
52
+ axiosRetry(this.axios, {
53
+ retries: 3, // Number of retries (Defaults to 3)
54
+ shouldResetTimeout: true,
55
+ retryCondition: error => {
56
+ // Conditional check the error status code
57
+ switch (error.response.status) {
58
+ case 409:
59
+ case 429:
60
+ case 502:
61
+ case 503:
62
+ return true; // Retry request with response status code 409, 429, 502, 503
63
+ default:
64
+ return false; // Do not retry the others
65
+ }
66
+ },
67
+ retryDelay: retryCount => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
68
+ onRetry: retryCount => {
69
+ debug(`Retry attempt #${retryCount} failed. Retrying again...`);
70
+ },
71
+ });
72
+
36
73
  this.isEnabled = true;
37
74
  // do not finish this run (for parallel testing)
38
75
  this.proceed = process.env.TESTOMATIO_PROCEED;
76
+ this.jiraId = process.env.TESTOMATIO_JIRA_ID;
39
77
  this.runId = params.runId || process.env.runId;
40
- this.createNewTests = !!process.env.TESTOMATIO_CREATE;
78
+ this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
79
+ this.hasUnmatchedTests = false;
41
80
 
42
81
  if (!isValidUrl(this.url.trim())) {
43
82
  this.isEnabled = false;
@@ -45,91 +84,196 @@ class TestomatioPipe {
45
84
  }
46
85
  }
47
86
 
87
+ /**
88
+ * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
89
+ * @param {Object} opts - The options for preparing the test grepList.
90
+ * @returns {Promise<string[]>} - An array containing the retrieved
91
+ * test grepList, or an empty array if no tests are found or the request is disabled.
92
+ * @throws {Error} - Throws an error if there was a problem while making the request.
93
+ */
94
+ async prepareRun(opts) {
95
+ if (!this.isEnabled) return [];
96
+
97
+ const { type, id } = parseFilterParams(opts);
98
+
99
+ try {
100
+ const q = generateFilterRequestParams({
101
+ type,
102
+ id,
103
+ apiKey: this.apiKey.trim(),
104
+ });
105
+
106
+ if (!q) {
107
+ return;
108
+ }
109
+
110
+ const resp = await this.axios.get('/api/test_grep', q);
111
+ const { data } = resp;
112
+
113
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
114
+ foundedTestLog(APP_PREFIX, data.tests);
115
+ return data.tests;
116
+ }
117
+
118
+ console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
119
+ } catch (err) {
120
+ console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
121
+ }
122
+ }
123
+
48
124
  /**
49
125
  * @returns Promise<void>
50
126
  */
51
127
  async createRun() {
128
+ debug('Creating run...');
52
129
  if (!this.isEnabled) return;
130
+ this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
131
+
132
+ let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
133
+
134
+ // GitHub Actions Url
135
+ if (!buildUrl && process.env.GITHUB_RUN_ID) {
136
+ // eslint-disable-next-line max-len
137
+ buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
138
+ }
139
+
140
+ // Azure DevOps Url
141
+ if (!buildUrl && process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
142
+ const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
143
+ const project = process.env.SYSTEM_TEAMPROJECT;
144
+ const buildId = process.env.BUILD_BUILDID;
145
+ buildUrl = `${collectionUri}/${project}/_build/results?buildId=${buildId}`;
146
+ }
147
+
148
+ if (buildUrl && !buildUrl.startsWith('http')) buildUrl = undefined;
149
+
150
+ const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
53
151
 
54
152
  const runParams = Object.fromEntries(
55
153
  Object.entries({
154
+ ci_build_url: buildUrl,
155
+ parallel: this.parallel,
56
156
  api_key: this.apiKey.trim(),
57
157
  group_title: this.groupTitle,
158
+ access_event: accessEvent,
159
+ jira_id: this.jiraId,
58
160
  env: this.env,
59
161
  title: this.title,
162
+ label: this.label,
60
163
  shared_run: this.sharedRun,
61
- }).filter(([, value]) => !!value)
164
+ }).filter(([, value]) => !!value),
62
165
  );
166
+ debug('Run params', JSON.stringify(runParams, null, 2));
63
167
 
64
168
  if (this.runId) {
65
- const resp = await this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams);
169
+ debug(`Run with id ${this.runId} already created, updating...`);
170
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
66
171
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
67
172
  return;
68
173
  }
69
174
 
70
175
  try {
71
- const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
176
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
72
177
  maxContentLength: Infinity,
73
178
  maxBodyLength: Infinity,
74
179
  });
180
+
75
181
  this.runId = resp.data.uid;
76
182
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
183
+ this.runPublicUrl = resp.data.public_url;
184
+
77
185
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
186
+
78
187
  this.store.runUrl = this.runUrl;
188
+ this.store.runPublicUrl = this.runPublicUrl;
79
189
  this.store.runId = this.runId;
80
190
  console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
81
191
  process.env.runId = this.runId;
192
+ debug('Run created', this.runId);
82
193
  } catch (err) {
83
194
  console.error(
84
195
  APP_PREFIX,
85
- 'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
196
+ 'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
197
+ err?.response?.statusText || err?.status || err.message,
86
198
  );
87
199
  }
200
+ debug('"createRun" function finished');
88
201
  }
89
202
 
90
203
  /**
91
- *
92
- * @param testData data
93
- * @returns
204
+ * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
205
+ */
206
+ #batchUpload = async () => {
207
+ if (!this.batch.tests.length) return;
208
+
209
+ // get tests from batch and clear batch
210
+ const testsToSend = this.batch.tests.splice(0);
211
+ debug('📨 Batch upload', testsToSend.length, 'tests');
212
+
213
+ return this.axios
214
+ .post(
215
+ `/api/reporter/${this.runId}/testrun`,
216
+ { api_key: this.apiKey, tests: testsToSend },
217
+ {
218
+ maxContentLength: Infinity,
219
+ maxBodyLength: Infinity,
220
+ headers: {
221
+ // Overwrite Axios's automatically set Content-Type
222
+ 'Content-Type': 'application/json',
223
+ },
224
+ },
225
+ )
226
+ .catch(err => {
227
+ if (err.response) {
228
+ if (err.response.status >= 400) {
229
+ const responseData = err.response.data || { message: '' };
230
+ console.log(
231
+ APP_PREFIX,
232
+ chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
233
+ // chalk.grey(data?.title || ''),
234
+ );
235
+ if (err.response.data.message.includes('could not be matched')) {
236
+ this.hasUnmatchedTests = true;
237
+ }
238
+ return;
239
+ }
240
+ console.log(
241
+ APP_PREFIX,
242
+ chalk.yellow(`Warning: (${err.response?.status})`),
243
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
244
+ );
245
+ } else {
246
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
247
+ }
248
+ });
249
+ };
250
+
251
+ /**
252
+ * Adds a test to the batch uploader
94
253
  */
95
254
  addTest(data) {
96
255
  if (!this.isEnabled) return;
97
256
  if (!this.runId) return;
98
257
  data.api_key = this.apiKey;
99
258
  data.create = this.createNewTests;
100
- const json = JsonCycle.stringify(data);
101
-
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: '' };
114
- console.log(
115
- APP_PREFIX,
116
- chalk.blue(this.title),
117
- `Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
118
- );
119
- return;
120
- }
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
- });
259
+ // const json = JsonCycle.stringify(data);
260
+ // debug('Adding test', JSON.stringify(data));
261
+
262
+ this.batch.tests.push(data);
263
+
264
+ // if test is added after run already finished
265
+ if (!this.batch.intervalFunction) this.#batchUpload();
126
266
  }
127
267
 
128
268
  /**
129
- * @param {import('../../types').RunData} params
130
- * @returns
269
+ * @param {import('../../types').RunData} params
270
+ * @returns
131
271
  */
132
272
  async finishRun(params) {
273
+ this.#batchUpload();
274
+ if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
275
+
276
+ debug('Finishing run...');
133
277
  if (!this.isEnabled) return;
134
278
 
135
279
  const { status, parallel } = params;
@@ -143,7 +287,7 @@ class TestomatioPipe {
143
287
 
144
288
  try {
145
289
  if (this.runId && !this.proceed) {
146
- await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
290
+ await this.axios.put(`/api/reporter/${this.runId}`, {
147
291
  api_key: this.apiKey,
148
292
  status_event,
149
293
  tests: params.tests,
@@ -151,15 +295,44 @@ class TestomatioPipe {
151
295
  if (this.runUrl) {
152
296
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
153
297
  }
298
+ if (this.runPublicUrl) {
299
+ console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
300
+ }
154
301
  }
155
302
  if (this.runUrl && this.proceed) {
156
303
  const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
157
304
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
158
305
  console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
159
306
  }
307
+ if (this.hasUnmatchedTests) {
308
+ console.log('');
309
+ // eslint-disable-next-line max-len
310
+ console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
311
+ // eslint-disable-next-line max-len
312
+ console.log(
313
+ APP_PREFIX,
314
+ `If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
315
+ );
316
+ // eslint-disable-next-line max-len
317
+ console.log(
318
+ APP_PREFIX,
319
+ `But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
320
+ );
321
+ console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
322
+ console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
323
+ console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
324
+ console.log(APP_PREFIX, 'or for Cucumber:');
325
+ // eslint-disable-next-line max-len
326
+ console.log(
327
+ APP_PREFIX,
328
+ chalk.bold('npx check-cucumber ... --update-ids'),
329
+ 'See: https://bit.ly/bdd-update-ids',
330
+ );
331
+ }
160
332
  } catch (err) {
161
333
  console.log(APP_PREFIX, 'Error updating status, skipping...', err);
162
334
  }
335
+ debug('Run finished');
163
336
  }
164
337
 
165
338
  toString() {
@@ -168,18 +341,3 @@ class TestomatioPipe {
168
341
  }
169
342
 
170
343
  module.exports = TestomatioPipe;
171
-
172
-
173
- function setS3Credentials(artifacts) {
174
- if (!Object.keys(artifacts).length) return;
175
-
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();
185
- }
@@ -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();