@testomatio/reporter 1.2.2-beta → 1.2.2-beta-html-pagination-feature-v2

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 (46) 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 +363 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +183 -56
  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/testomatio.hbs +1233 -0
  39. package/lib/utils/pipe_utils.js +128 -0
  40. package/lib/{util.js → utils/utils.js} +144 -12
  41. package/lib/xmlReader.js +211 -122
  42. package/package.json +18 -8
  43. package/lib/_ArtifactStorageOld.js +0 -142
  44. package/lib/artifactStorage.js +0 -25
  45. package/lib/dataStorage.js +0 -180
  46. 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,121 @@ 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
  );
87
193
  }
194
+ debug('"createRun" function finished');
88
195
  }
89
196
 
90
- /**
91
- *
92
- * @param testData data
93
- * @returns
94
- */
95
197
  addTest(data) {
96
198
  if (!this.isEnabled) return;
97
199
  if (!this.runId) return;
@@ -99,37 +201,48 @@ class TestomatioPipe {
99
201
  data.create = this.createNewTests;
100
202
  const json = JsonCycle.stringify(data);
101
203
 
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: '' };
204
+ debug('Adding test', json);
205
+
206
+ return this.axios
207
+ .post(`/api/reporter/${this.runId}/testrun`, json, {
208
+ maxContentLength: Infinity,
209
+ maxBodyLength: Infinity,
210
+ headers: {
211
+ // Overwrite Axios's automatically set Content-Type
212
+ 'Content-Type': 'application/json',
213
+ },
214
+ })
215
+ .catch(err => {
216
+ if (err.response) {
217
+ if (err.response.status >= 400) {
218
+ const responseData = err.response.data || { message: '' };
219
+ console.log(
220
+ APP_PREFIX,
221
+ chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
222
+ chalk.grey(data?.title || ''),
223
+ );
224
+ if (err.response.data.message.includes('could not be matched')) {
225
+ this.hasUnmatchedTests = true;
226
+ }
227
+ return;
228
+ }
114
229
  console.log(
115
230
  APP_PREFIX,
116
- chalk.blue(this.title),
117
- `Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
231
+ chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
232
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
118
233
  );
119
- return;
234
+ } else {
235
+ console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
120
236
  }
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
- });
237
+ });
126
238
  }
127
239
 
128
240
  /**
129
- * @param {import('../../types').RunData} params
130
- * @returns
241
+ * @param {import('../../types').RunData} params
242
+ * @returns
131
243
  */
132
244
  async finishRun(params) {
245
+ debug('Finishing run...');
133
246
  if (!this.isEnabled) return;
134
247
 
135
248
  const { status, parallel } = params;
@@ -143,7 +256,7 @@ class TestomatioPipe {
143
256
 
144
257
  try {
145
258
  if (this.runId && !this.proceed) {
146
- await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
259
+ await this.axios.put(`/api/reporter/${this.runId}`, {
147
260
  api_key: this.apiKey,
148
261
  status_event,
149
262
  tests: params.tests,
@@ -151,15 +264,44 @@ class TestomatioPipe {
151
264
  if (this.runUrl) {
152
265
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
153
266
  }
267
+ if (this.runPublicUrl) {
268
+ console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
269
+ }
154
270
  }
155
271
  if (this.runUrl && this.proceed) {
156
272
  const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
157
273
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
158
274
  console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
159
275
  }
276
+ if (this.hasUnmatchedTests) {
277
+ console.log('');
278
+ // eslint-disable-next-line max-len
279
+ console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
280
+ // eslint-disable-next-line max-len
281
+ console.log(
282
+ APP_PREFIX,
283
+ `If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
284
+ );
285
+ // eslint-disable-next-line max-len
286
+ console.log(
287
+ APP_PREFIX,
288
+ `But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
289
+ );
290
+ console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
291
+ console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
292
+ console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
293
+ console.log(APP_PREFIX, 'or for Cucumber:');
294
+ // eslint-disable-next-line max-len
295
+ console.log(
296
+ APP_PREFIX,
297
+ chalk.bold('npx check-cucumber ... --update-ids'),
298
+ 'See: https://bit.ly/bdd-update-ids',
299
+ );
300
+ }
160
301
  } catch (err) {
161
302
  console.log(APP_PREFIX, 'Error updating status, skipping...', err);
162
303
  }
304
+ debug('Run finished');
163
305
  }
164
306
 
165
307
  toString() {
@@ -168,18 +310,3 @@ class TestomatioPipe {
168
310
  }
169
311
 
170
312
  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();