@testomatio/reporter 1.3.6-beta → 1.4.0-beta-csv-enable

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 +60 -57
  2. package/lib/adapter/codecept.js +96 -35
  3. package/lib/adapter/cucumber/current.js +19 -12
  4. package/lib/adapter/cucumber/legacy.js +7 -6
  5. package/lib/adapter/cucumber.js +2 -2
  6. package/lib/adapter/cypress-plugin/index.js +53 -26
  7. package/lib/adapter/jasmine.js +2 -2
  8. package/lib/adapter/jest.js +50 -17
  9. package/lib/adapter/mocha.js +110 -58
  10. package/lib/adapter/playwright.js +95 -33
  11. package/lib/adapter/webdriver.js +2 -2
  12. package/lib/bin/reportXml.js +22 -16
  13. package/lib/bin/startTest.js +27 -6
  14. package/lib/client.js +179 -53
  15. package/lib/config.js +34 -0
  16. package/lib/constants.js +32 -7
  17. package/lib/data-storage.js +203 -0
  18. package/lib/fileUploader.js +135 -54
  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 +37 -31
  27. package/lib/pipe/github.js +9 -19
  28. package/lib/pipe/gitlab.js +22 -26
  29. package/lib/pipe/html.js +361 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +256 -53
  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/emptyData.svg +23 -0
  39. package/lib/template/testomatio.hbs +1421 -0
  40. package/lib/utils/pipe_utils.js +129 -0
  41. package/lib/{util.js → utils/utils.js} +147 -17
  42. package/lib/xmlReader.js +211 -122
  43. package/package.json +17 -9
  44. package/lib/_ArtifactStorageOld.js +0 -142
  45. package/lib/artifactStorage.js +0 -25
  46. package/lib/dataStorage.js +0 -244
  47. package/lib/logger.js +0 -301
package/lib/pipe/index.js CHANGED
@@ -6,6 +6,7 @@ const TestomatioPipe = require('./testomatio');
6
6
  const GitHubPipe = require('./github');
7
7
  const GitLabPipe = require('./gitlab');
8
8
  const CsvPipe = require('./csv');
9
+ const HtmlPipe = require('./html');
9
10
 
10
11
  function PipeFactory(params, opts) {
11
12
  const extraPipes = [];
@@ -43,6 +44,7 @@ function PipeFactory(params, opts) {
43
44
  new GitHubPipe(params, opts),
44
45
  new GitLabPipe(params, opts),
45
46
  new CsvPipe(params, opts),
47
+ new HtmlPipe(params, opts),
46
48
  ...extraPipes,
47
49
  ];
48
50
 
@@ -60,4 +62,4 @@ function PipeFactory(params, opts) {
60
62
  return pipes;
61
63
  }
62
64
 
63
- module.exports = PipeFactory;
65
+ module.exports = PipeFactory;
@@ -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, REPORTER_REQUEST_RETRIES } = 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
  /**
@@ -19,9 +23,13 @@ if (TESTOMATIO_RUN) {
19
23
  */
20
24
  class TestomatioPipe {
21
25
  constructor(params, store) {
26
+ this.retriesTimestamps = [];
27
+ this.reportingCanceledDueToReqFailures = false;
28
+ this.notReportedTestsCount = 0;
29
+
22
30
  this.isEnabled = false;
23
31
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
24
- this.apiKey = params.apiKey || process.env.TESTOMATIO;
32
+ this.apiKey = params.apiKey || config.TESTOMATIO;
25
33
  debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
26
34
  if (!this.apiKey) {
27
35
  return;
@@ -33,12 +41,46 @@ class TestomatioPipe {
33
41
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
34
42
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
35
43
  this.env = process.env.TESTOMATIO_ENV;
36
- this.axios = axios.create();
44
+ this.label = process.env.TESTOMATIO_LABEL;
45
+ // Create a new instance of axios with a custom config
46
+ this.axios = axios.create({
47
+ baseURL: `${this.url.trim()}`,
48
+ timeout: AXIOS_TIMEOUT,
49
+ });
50
+
51
+ // Pass the axios instance to the retry function
52
+ axiosRetry(this.axios, {
53
+ // do not use retries for unit tests
54
+ retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
55
+ shouldResetTimeout: true,
56
+ retryCondition: error => {
57
+ if (!error.response) return false;
58
+ switch (error.response?.status) {
59
+ case 400: // Bad request (probably wrong API key)
60
+ case 404: // Test not matched
61
+ case 429: // Rate limit exceeded
62
+ case 500: // Internal server error
63
+ return false;
64
+ default:
65
+ break;
66
+ }
67
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
68
+ },
69
+ retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
70
+ onRetry: async (retryCount, error) => {
71
+ this.retriesTimestamps.push(Date.now());
72
+
73
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
74
+ },
75
+ });
76
+
37
77
  this.isEnabled = true;
38
78
  // do not finish this run (for parallel testing)
39
79
  this.proceed = process.env.TESTOMATIO_PROCEED;
80
+ this.jiraId = process.env.TESTOMATIO_JIRA_ID;
40
81
  this.runId = params.runId || process.env.runId;
41
- this.createNewTests = !!process.env.TESTOMATIO_CREATE;
82
+ this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
83
+ this.hasUnmatchedTests = false;
42
84
 
43
85
  if (!isValidUrl(this.url.trim())) {
44
86
  this.isEnabled = false;
@@ -46,93 +88,216 @@ class TestomatioPipe {
46
88
  }
47
89
  }
48
90
 
91
+ /**
92
+ * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
93
+ * @param {Object} opts - The options for preparing the test grepList.
94
+ * @returns {Promise<string[]>} - An array containing the retrieved
95
+ * test grepList, or an empty array if no tests are found or the request is disabled.
96
+ * @throws {Error} - Throws an error if there was a problem while making the request.
97
+ */
98
+ async prepareRun(opts) {
99
+ if (!this.isEnabled) return [];
100
+
101
+ const { type, id } = parseFilterParams(opts);
102
+
103
+ try {
104
+ const q = generateFilterRequestParams({
105
+ type,
106
+ id,
107
+ apiKey: this.apiKey.trim(),
108
+ });
109
+
110
+ if (!q) {
111
+ return;
112
+ }
113
+
114
+ const resp = await this.axios.get('/api/test_grep', q);
115
+ const { data } = resp;
116
+
117
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
118
+ foundedTestLog(APP_PREFIX, data.tests);
119
+ return data.tests;
120
+ }
121
+
122
+ console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
123
+ } catch (err) {
124
+ console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
125
+ }
126
+ }
127
+
49
128
  /**
50
129
  * @returns Promise<void>
51
130
  */
52
131
  async createRun() {
132
+ debug('Creating run...');
53
133
  if (!this.isEnabled) return;
54
134
 
135
+ let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
136
+
137
+ // GitHub Actions Url
138
+ if (!buildUrl && process.env.GITHUB_RUN_ID) {
139
+ // eslint-disable-next-line max-len
140
+ buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
141
+ }
142
+
143
+ // Azure DevOps Url
144
+ if (!buildUrl && process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
145
+ const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
146
+ const project = process.env.SYSTEM_TEAMPROJECT;
147
+ const buildId = process.env.BUILD_BUILDID;
148
+ buildUrl = `${collectionUri}/${project}/_build/results?buildId=${buildId}`;
149
+ }
150
+
151
+ if (buildUrl && !buildUrl.startsWith('http')) buildUrl = undefined;
152
+
153
+ const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
154
+
55
155
  const runParams = Object.fromEntries(
56
156
  Object.entries({
157
+ ci_build_url: buildUrl,
57
158
  parallel: this.parallel,
58
159
  api_key: this.apiKey.trim(),
59
160
  group_title: this.groupTitle,
161
+ access_event: accessEvent,
162
+ jira_id: this.jiraId,
60
163
  env: this.env,
61
164
  title: this.title,
165
+ label: this.label,
62
166
  shared_run: this.sharedRun,
63
- }).filter(([, value]) => !!value)
167
+ }).filter(([, value]) => !!value),
64
168
  );
169
+ debug('Run params', JSON.stringify(runParams, null, 2));
65
170
 
66
171
  if (this.runId) {
67
- const resp = await this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams);
172
+ debug(`Run with id ${this.runId} already created, updating...`);
173
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
68
174
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
69
175
  return;
70
176
  }
71
177
 
72
178
  try {
73
- const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
179
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
74
180
  maxContentLength: Infinity,
75
181
  maxBodyLength: Infinity,
76
182
  });
183
+
77
184
  this.runId = resp.data.uid;
78
185
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
186
+ this.runPublicUrl = resp.data.public_url;
187
+
79
188
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
189
+
80
190
  this.store.runUrl = this.runUrl;
191
+ this.store.runPublicUrl = this.runPublicUrl;
81
192
  this.store.runId = this.runId;
82
193
  console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
83
194
  process.env.runId = this.runId;
195
+ debug('Run created', this.runId);
84
196
  } catch (err) {
85
197
  console.error(
86
198
  APP_PREFIX,
87
- 'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
199
+ 'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
200
+ err?.response?.statusText || err?.status || err.message,
88
201
  );
202
+ printCreateIssue(err);
89
203
  }
204
+ debug('"createRun" function finished');
90
205
  }
91
206
 
92
207
  /**
93
- *
94
- * @param testData data
95
- * @returns
208
+ * Decides whether to skip test reporting in case of too many request failures
209
+ * @param {TestData} testData
210
+ * @returns {boolean}
96
211
  */
212
+ #cancelTestReportingInCaseOfTooManyReqFailures(testData) {
213
+ if (this.reportingCanceledDueToReqFailures) return true;
214
+
215
+ const retriesCountWithinTime = this.retriesTimestamps.filter(
216
+ timestamp => Date.now() - timestamp < REPORTER_REQUEST_RETRIES.withinTimeSeconds * 1000,
217
+ ).length;
218
+ debug(`${retriesCountWithinTime} failed requests within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s`);
219
+
220
+ if (retriesCountWithinTime > REPORTER_REQUEST_RETRIES.maxTotalRetries) {
221
+ const errorMessage = chalk.yellow(
222
+ `${retriesCountWithinTime} requests were failed within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s,\
223
+ reporting for test "${testData.title}" to Testomat is skipped`,
224
+ );
225
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
226
+
227
+ this.reportingCanceledDueToReqFailures = true;
228
+ this.notReportedTestsCount++;
229
+
230
+ return true;
231
+ }
232
+
233
+ return false;
234
+ }
235
+
97
236
  addTest(data) {
98
237
  if (!this.isEnabled) return;
99
238
  if (!this.runId) return;
239
+ if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
240
+
100
241
  data.api_key = this.apiKey;
101
242
  data.create = this.createNewTests;
243
+
244
+ if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
245
+ data.stack = null;
246
+ }
247
+
102
248
  const json = JsonCycle.stringify(data);
103
249
 
104
- return this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
105
- maxContentLength: Infinity,
106
- maxBodyLength: Infinity,
107
- headers: {
108
- // Overwrite Axios's automatically set Content-Type
109
- 'Content-Type': 'application/json',
110
- },
111
- })
112
- .catch((err) => {
113
- if (err.response) {
114
- if (err.response.status >= 400) {
115
- const responseData = err.response.data || { message: '' };
250
+ debug('Adding test', json);
251
+
252
+ return this.axios
253
+ .post(`/api/reporter/${this.runId}/testrun`, json, {
254
+ maxContentLength: Infinity,
255
+ maxBodyLength: Infinity,
256
+ headers: {
257
+ // Overwrite Axios's automatically set Content-Type
258
+ 'Content-Type': 'application/json',
259
+ },
260
+ })
261
+ .catch(err => {
262
+ if (err.response) {
263
+ if (err.response.status >= 400) {
264
+ const responseData = err.response.data || { message: '' };
265
+ console.log(
266
+ APP_PREFIX,
267
+ chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
268
+ chalk.grey(data?.title || ''),
269
+ );
270
+ if (err.response?.data?.message?.includes('could not be matched')) {
271
+ this.hasUnmatchedTests = true;
272
+ }
273
+ return;
274
+ }
116
275
  console.log(
117
276
  APP_PREFIX,
118
- chalk.blue(this.title),
119
- `Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
277
+ chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
278
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
120
279
  );
121
- return;
280
+ printCreateIssue(err);
281
+ } else {
282
+ console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
122
283
  }
123
- console.log(APP_PREFIX, chalk.blue(this.title), `Report couldn't be processed: ${err.response.data.message}`);
124
- } else {
125
- console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
126
- }
127
- });
284
+ });
128
285
  }
129
286
 
130
287
  /**
131
- * @param {import('../../types').RunData} params
132
- * @returns
288
+ * @param {import('../../types').RunData} params
289
+ * @returns
133
290
  */
134
291
  async finishRun(params) {
135
292
  if (!this.isEnabled) return;
293
+ debug('Finishing run...');
294
+
295
+ if (this.reportingCanceledDueToReqFailures) {
296
+ const errorMessage = chalk.red(
297
+ `⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
298
+ );
299
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
300
+ }
136
301
 
137
302
  const { status, parallel } = params;
138
303
 
@@ -145,7 +310,7 @@ class TestomatioPipe {
145
310
 
146
311
  try {
147
312
  if (this.runId && !this.proceed) {
148
- await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
313
+ await this.axios.put(`/api/reporter/${this.runId}`, {
149
314
  api_key: this.apiKey,
150
315
  status_event,
151
316
  tests: params.tests,
@@ -153,15 +318,45 @@ class TestomatioPipe {
153
318
  if (this.runUrl) {
154
319
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
155
320
  }
321
+ if (this.runPublicUrl) {
322
+ console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
323
+ }
156
324
  }
157
325
  if (this.runUrl && this.proceed) {
158
326
  const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
159
327
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
160
328
  console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
161
329
  }
330
+ if (this.hasUnmatchedTests) {
331
+ console.log('');
332
+ // eslint-disable-next-line max-len
333
+ console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
334
+ // eslint-disable-next-line max-len
335
+ console.log(
336
+ APP_PREFIX,
337
+ `If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
338
+ );
339
+ // eslint-disable-next-line max-len
340
+ console.log(
341
+ APP_PREFIX,
342
+ `But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
343
+ );
344
+ console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
345
+ console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
346
+ console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
347
+ console.log(APP_PREFIX, 'or for Cucumber:');
348
+ // eslint-disable-next-line max-len
349
+ console.log(
350
+ APP_PREFIX,
351
+ chalk.bold('npx check-cucumber ... --update-ids'),
352
+ 'See: https://bit.ly/bdd-update-ids',
353
+ );
354
+ }
162
355
  } catch (err) {
163
356
  console.log(APP_PREFIX, 'Error updating status, skipping...', err);
357
+ printCreateIssue(err);
164
358
  }
359
+ debug('Run finished');
165
360
  }
166
361
 
167
362
  toString() {
@@ -169,19 +364,27 @@ class TestomatioPipe {
169
364
  }
170
365
  }
171
366
 
172
- module.exports = TestomatioPipe;
173
-
367
+ let registeredErrorHints = false;
368
+ function printCreateIssue(err) {
369
+ if (registeredErrorHints) return;
370
+ registeredErrorHints = true;
371
+ process.on('exit', () => {
372
+ console.log();
373
+ console.log(APP_PREFIX, 'There was an error reporting to Testomat.io:');
374
+ console.log(
375
+ APP_PREFIX,
376
+ 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new',
377
+ ); // eslint-disable-line max-len
378
+ console.log(APP_PREFIX, 'Provide this information:');
379
+ console.log('Error:', err.message || err.code);
380
+ if (!err.config) return;
174
381
 
175
- function setS3Credentials(artifacts) {
176
- if (!Object.keys(artifacts).length) return;
177
-
178
- console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
179
-
180
- if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
181
- if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
182
- if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
183
- if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
184
- if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
185
- if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
186
- resetConfig();
382
+ const time = new Date().toUTCString();
383
+ const { data, url, baseURL, method } = err?.config || {};
384
+ console.log('```js');
385
+ console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
386
+ console.log('```');
387
+ });
187
388
  }
389
+
390
+ 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();