@testomatio/reporter 1.3.6-beta → 1.4.0-beta-wdio-bdd

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 +60 -57
  2. package/lib/adapter/codecept.js +96 -35
  3. package/lib/adapter/cucumber/current.js +18 -11
  4. package/lib/adapter/cucumber/legacy.js +6 -5
  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 +95 -33
  11. package/lib/adapter/webdriver.js +46 -1
  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 +5 -3
  27. package/lib/pipe/github.js +9 -19
  28. package/lib/pipe/gitlab.js +22 -26
  29. package/lib/pipe/html.js +354 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +257 -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/testomatio.hbs +1236 -0
  39. package/lib/utils/pipe_utils.js +129 -0
  40. package/lib/{util.js → utils/utils.js} +145 -15
  41. package/lib/xmlReader.js +211 -122
  42. package/package.json +17 -9
  43. package/lib/_ArtifactStorageOld.js +0 -142
  44. package/lib/artifactStorage.js +0 -25
  45. package/lib/dataStorage.js +0 -244
  46. package/lib/logger.js +0 -301
@@ -0,0 +1,129 @@
1
+ const { resetConfig } = require('../fileUploader');
2
+ const { APP_PREFIX } = require('../constants');
3
+
4
+ /**
5
+ * Set S3 credentials from the provided artifacts object.
6
+ * @param {Object} artifacts - The artifacts object containing S3 credentials.
7
+ */
8
+ function setS3Credentials(artifacts) {
9
+ if (!Object.keys(artifacts).length) return;
10
+
11
+ console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
12
+
13
+ if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
14
+ if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
15
+ if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
16
+ if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
17
+ if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
18
+ if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
19
+ if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
20
+ resetConfig();
21
+ }
22
+ /**
23
+ * Generates mode request parameters based on the input params.
24
+ * @param {Object} params - The input parameters for the request.
25
+ * @param {string} params.type - The type of the request (e.g., "tag").
26
+ * @param {string} params.id - The ID associated with the request.
27
+ * @param {string} params.apiKey - The API key for authentication.
28
+ * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
29
+ */
30
+ function generateFilterRequestParams(params) {
31
+ const { type, id, apiKey } = params;
32
+
33
+ if (!type) {
34
+ return;
35
+ }
36
+
37
+ if (!id) {
38
+ console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
39
+ return;
40
+ }
41
+
42
+ return {
43
+ params: {
44
+ type,
45
+ id: encodeURIComponent(id),
46
+ api_key: apiKey,
47
+ },
48
+ responseType: 'json',
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Parse filter parameters from a string in the format "type=id".
54
+ * @param {string} opts - The input string containing the filter parameters.
55
+ * @returns {Object} An object containing the parsed filter parameters.
56
+ * The object has properties "type" and "id".
57
+ */
58
+ function parseFilterParams(opts) {
59
+ const [type, id] = opts.split('=');
60
+ const validType = updateFilterType(type);
61
+
62
+ return {
63
+ type: validType,
64
+ id,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Update and validate the filter type.
70
+ * @param {string} type - The original filter type.
71
+ * @returns {string|undefined} The updated and validated filter type.
72
+ * Returns undefined if the type is not valid.
73
+ */
74
+ function updateFilterType(type) {
75
+ const typeLowerCase = type.toLowerCase();
76
+
77
+ const filterTypes = ['tag-name', 'plan-id', 'label', 'jira-ticket'];
78
+
79
+ const filterApi = [
80
+ 'tag',
81
+ 'plan',
82
+ 'label',
83
+ 'jira',
84
+ // "ims-issue", //TODO: WIP
85
+ ];
86
+
87
+ if (!filterTypes.includes(typeLowerCase)) {
88
+ console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
89
+ return;
90
+ }
91
+
92
+ const index = filterTypes.indexOf(typeLowerCase);
93
+
94
+ return index !== -1 ? filterApi[index] : undefined;
95
+ }
96
+
97
+ /**
98
+ * Return an emoji based on the provided status.
99
+ * @param {string} status - The status value ('passed', 'failed', or 'skipped').
100
+ * @returns {string} - An emoji corresponding to the provided status.
101
+ */
102
+ function statusEmoji(status) {
103
+ if (status === 'passed') return '🟢';
104
+ if (status === 'failed') return '🔴';
105
+ if (status === 'skipped') return '🟡';
106
+ return '';
107
+ }
108
+
109
+ /**
110
+ * Generate a full name string based on the provided test object.
111
+ * @param {object} t - The test object.
112
+ * @returns {string} - A formatted full name string for the test object.
113
+ */
114
+ function fullName(t) {
115
+ let line = '';
116
+ if (t.suite_title) line = `${t.suite_title}: `;
117
+ line += `**${t.title}**`;
118
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
119
+ return line;
120
+ }
121
+
122
+ module.exports = {
123
+ updateFilterType,
124
+ parseFilterParams,
125
+ generateFilterRequestParams,
126
+ setS3Credentials,
127
+ statusEmoji,
128
+ fullName,
129
+ };
@@ -12,11 +12,11 @@ const debug = require('debug')('@testomatio/reporter:util');
12
12
  */
13
13
  const parseTest = testTitle => {
14
14
  if (!testTitle) return null;
15
-
16
- const captures = testTitle.match(/@T([\w\d]+)/);
17
-
15
+
16
+ const captures = testTitle.match(/@T[\w\d]{8}/);
17
+
18
18
  if (captures) {
19
- return captures[1];
19
+ return captures[0];
20
20
  }
21
21
 
22
22
  return null;
@@ -28,7 +28,7 @@ const parseTest = testTitle => {
28
28
  * @returns {String|null} suiteId
29
29
  */
30
30
  const parseSuite = suiteTitle => {
31
- const captures = suiteTitle.match(/@S([\w\d]+)/);
31
+ const captures = suiteTitle.match(/@S[\w\d]{8}/);
32
32
  if (captures) {
33
33
  return captures[1];
34
34
  }
@@ -55,11 +55,20 @@ const isValidUrl = s => {
55
55
  }
56
56
  };
57
57
 
58
+ const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/gi;
59
+
58
60
  const fetchFilesFromStackTrace = (stack = '') => {
59
- const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
60
- return Array.from(files)
61
- .map(f => f[1])
62
- .filter(f => fs.existsSync(f));
61
+ const files = Array.from(stack.matchAll(fileMatchRegex))
62
+ .map(f => f[1].trim())
63
+ .map(f => (f.startsWith('//') ? f.substring(1) : f));
64
+
65
+ debug('Found files in stack trace: ', files);
66
+
67
+ return files.filter(f => {
68
+ const isFile = fs.existsSync(f);
69
+ if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
70
+ return isFile;
71
+ });
63
72
  };
64
73
 
65
74
  const fetchSourceCodeFromStackTrace = (stack = '') => {
@@ -87,11 +96,41 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
87
96
 
88
97
  if (!source) return '';
89
98
 
90
- return source.split('\n')
99
+ return source
100
+ .split('\n')
91
101
  .map((l, i) => {
92
102
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
93
- return `${line - prepend + i} | ${l}`
94
- }).join('\n');
103
+ return `${line - prepend + i} | ${l}`;
104
+ })
105
+ .join('\n');
106
+ };
107
+
108
+ const TEST_ID_REGEX = /@T([\w\d]{8})/;
109
+
110
+ const fetchIdFromCode = (code, opts = {}) => {
111
+ const comments = code
112
+ .split('\n')
113
+ .map(l => l.trim())
114
+ .filter(l => {
115
+ switch (opts.lang) {
116
+ case 'ruby':
117
+ case 'python':
118
+ return l.startsWith('# ');
119
+ default:
120
+ return l.startsWith('// ');
121
+ }
122
+ });
123
+
124
+ return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
125
+ };
126
+
127
+ const fetchIdFromOutput = output => {
128
+ const lines = output
129
+ .split('\n')
130
+ .map(l => l.trim())
131
+ .filter(l => l.startsWith('tid://'));
132
+
133
+ return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
95
134
  };
96
135
 
97
136
  const fetchSourceCode = (contents, opts = {}) => {
@@ -106,7 +145,15 @@ const fetchSourceCode = (contents, opts = {}) => {
106
145
  // remove special chars from title
107
146
  if (!lineIndex && opts.title) {
108
147
  const title = opts.title.replace(/[([@].*/g, '');
109
- lineIndex = lines.findIndex(l => l.includes(title));
148
+
149
+ if (opts.lang === 'java') {
150
+ lineIndex = lines.findIndex(l => l.includes(`test${title}`));
151
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
152
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
153
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
154
+ } else {
155
+ lineIndex = lines.findIndex(l => l.includes(title));
156
+ }
110
157
  }
111
158
 
112
159
  if (opts.prepend) {
@@ -115,7 +162,7 @@ const fetchSourceCode = (contents, opts = {}) => {
115
162
 
116
163
  if (lineIndex) {
117
164
  const result = [];
118
- for (let i = lineIndex; i < (lineIndex + limit); i++) {
165
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
119
166
  if (lines[i] === undefined) continue;
120
167
 
121
168
  if (i > lineIndex + 2 && !opts.prepend) {
@@ -189,13 +236,92 @@ const fileSystem = {
189
236
  } else {
190
237
  debug(`Trying to delete ${dirPath} but it doesn't exist`);
191
238
  }
192
- }
239
+ },
240
+ };
241
+
242
+ const foundedTestLog = (app, tests) => {
243
+ const n = tests.length;
244
+
245
+ return n === 1 ? console.log(app, `✅ We found one test!`) : console.log(app, `✅ We found ${n} tests!`);
246
+ };
247
+
248
+ const humanize = text => {
249
+ text = decamelize(text);
250
+ return text
251
+ .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
252
+ .trim()
253
+ .replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
254
+ .trim()
255
+ .replace(/\sA\s/g, ' a ') // replace a|the
256
+ .replace(/\sThe\s/g, ' the ') // replace a|the
257
+ .replace(/^Test\s/, '')
258
+ .replace(/^Should\s/, '');
259
+ };
260
+
261
+ /**
262
+ * From https://github.com/sindresorhus/decamelize/blob/main/index.js
263
+ * @param {*} text
264
+ * @returns
265
+ */
266
+ const decamelize = text => {
267
+ const separator = '_';
268
+ const replacement = `$1${separator}$2`;
269
+
270
+ // Split lowercase sequences followed by uppercase character.
271
+ // `dataForUSACounties` → `data_For_USACounties`
272
+ // `myURLstring → `my_URLstring`
273
+ let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
274
+
275
+ // Lowercase all single uppercase characters. As we
276
+ // want to preserve uppercase sequences, we cannot
277
+ // simply lowercase the separated string at the end.
278
+ // `data_For_USACounties` → `data_for_USACounties`
279
+ decamelized = decamelized.replace(
280
+ /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
281
+ $0 => $0.toLowerCase(),
282
+ );
283
+
284
+ // Remaining uppercase sequences will be separated from lowercase sequences.
285
+ // `data_For_USACounties` → `data_for_USA_counties`
286
+ return decamelized.replace(
287
+ /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
288
+ (_, $1, $2) => $1 + separator + $2.toLowerCase(),
289
+ );
290
+ };
291
+
292
+ /**
293
+ * Used to remove color codes
294
+ * @param {*} input
295
+ * @returns
296
+ */
297
+ function removeColorCodes(input) {
298
+ // eslint-disable-next-line no-control-regex
299
+ return input.replace(/\x1b\[[0-9;]*m/g, '');
300
+ }
301
+
302
+ const testRunnerHelper = {
303
+ // for Jest
304
+ getNameOfCurrentlyRunningTest: () => {
305
+ if (global.testomatioTestTitle) return global.testomatioTestTitle;
306
+
307
+ if (!process.env.JEST_WORKER_ID) return null;
308
+ try {
309
+ // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
310
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
311
+ // eslint-disable-next-line no-undef
312
+ return expect?.getState()?.currentTestName;
313
+ } catch (e) {
314
+ return null;
315
+ }
316
+ },
193
317
  };
194
318
 
195
319
  module.exports = {
196
320
  isSameTest,
197
321
  fetchSourceCode,
198
322
  fetchSourceCodeFromStackTrace,
323
+ fetchIdFromCode,
324
+ fetchIdFromOutput,
199
325
  fetchFilesFromStackTrace,
200
326
  fileSystem,
201
327
  getCurrentDateTime,
@@ -204,4 +330,8 @@ module.exports = {
204
330
  ansiRegExp,
205
331
  parseTest,
206
332
  parseSuite,
333
+ humanize,
334
+ removeColorCodes,
335
+ foundedTestLog,
336
+ testRunnerHelper,
207
337
  };