@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.1-codeceptjs

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 (80) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.js +288 -202
  3. package/lib/adapter/cypress-plugin/index.js +0 -2
  4. package/lib/adapter/mocha.js +0 -1
  5. package/lib/adapter/nightwatch.js +5 -5
  6. package/lib/adapter/playwright.js +11 -3
  7. package/lib/adapter/webdriver.d.ts +1 -1
  8. package/lib/adapter/webdriver.js +18 -8
  9. package/lib/bin/cli.js +73 -8
  10. package/lib/bin/reportXml.js +4 -2
  11. package/lib/bin/startTest.js +3 -2
  12. package/lib/bin/uploadArtifacts.js +5 -4
  13. package/lib/client.js +30 -10
  14. package/lib/data-storage.d.ts +5 -5
  15. package/lib/data-storage.js +23 -13
  16. package/lib/junit-adapter/csharp.d.ts +1 -0
  17. package/lib/junit-adapter/csharp.js +11 -1
  18. package/lib/pipe/bitbucket.d.ts +2 -0
  19. package/lib/pipe/bitbucket.js +38 -26
  20. package/lib/pipe/debug.js +27 -6
  21. package/lib/pipe/github.d.ts +2 -2
  22. package/lib/pipe/github.js +35 -3
  23. package/lib/pipe/gitlab.d.ts +2 -0
  24. package/lib/pipe/gitlab.js +27 -9
  25. package/lib/pipe/html.js +0 -3
  26. package/lib/pipe/index.js +17 -7
  27. package/lib/pipe/testomatio.d.ts +3 -2
  28. package/lib/pipe/testomatio.js +85 -75
  29. package/lib/replay.d.ts +31 -0
  30. package/lib/replay.js +255 -0
  31. package/lib/reporter-functions.d.ts +7 -0
  32. package/lib/reporter-functions.js +36 -0
  33. package/lib/reporter.d.ts +15 -12
  34. package/lib/reporter.js +4 -1
  35. package/lib/services/artifacts.d.ts +1 -1
  36. package/lib/services/index.d.ts +2 -0
  37. package/lib/services/index.js +2 -0
  38. package/lib/services/key-values.d.ts +1 -1
  39. package/lib/services/labels.d.ts +22 -0
  40. package/lib/services/labels.js +62 -0
  41. package/lib/services/logger.d.ts +1 -1
  42. package/lib/services/logger.js +1 -2
  43. package/lib/template/testomatio.hbs +443 -68
  44. package/lib/uploader.js +10 -6
  45. package/lib/utils/constants.d.ts +12 -0
  46. package/lib/utils/constants.js +15 -0
  47. package/lib/utils/utils.d.ts +10 -1
  48. package/lib/utils/utils.js +70 -22
  49. package/lib/xmlReader.js +54 -19
  50. package/package.json +16 -11
  51. package/src/adapter/codecept.js +320 -214
  52. package/src/adapter/cypress-plugin/index.js +0 -2
  53. package/src/adapter/mocha.js +0 -1
  54. package/src/adapter/nightwatch.js +1 -1
  55. package/src/adapter/playwright.js +10 -7
  56. package/src/adapter/webdriver.js +2 -2
  57. package/src/bin/cli.js +70 -2
  58. package/src/bin/reportXml.js +4 -1
  59. package/src/bin/startTest.js +2 -1
  60. package/src/bin/uploadArtifacts.js +2 -1
  61. package/src/client.js +18 -3
  62. package/src/data-storage.js +6 -6
  63. package/src/junit-adapter/csharp.js +13 -1
  64. package/src/pipe/bitbucket.js +22 -24
  65. package/src/pipe/debug.js +26 -5
  66. package/src/pipe/github.js +1 -2
  67. package/src/pipe/gitlab.js +27 -9
  68. package/src/pipe/html.js +1 -4
  69. package/src/pipe/testomatio.js +106 -105
  70. package/src/replay.js +262 -0
  71. package/src/reporter-functions.js +41 -0
  72. package/src/reporter.js +3 -0
  73. package/src/services/index.js +2 -0
  74. package/src/services/labels.js +59 -0
  75. package/src/services/logger.js +1 -2
  76. package/src/template/testomatio.hbs +443 -68
  77. package/src/uploader.js +11 -6
  78. package/src/utils/constants.js +12 -0
  79. package/src/utils/utils.js +46 -13
  80. package/src/xmlReader.js +70 -18
@@ -5,9 +5,16 @@ import fs from 'fs';
5
5
  import isValid from 'is-valid-path';
6
6
  import createDebugMessages from 'debug';
7
7
  import os from 'os';
8
+ import { fileURLToPath } from 'url';
8
9
 
9
10
  const debug = createDebugMessages('@testomatio/reporter:util');
10
11
 
12
+ // Use __dirname directly since we're compiling to CommonJS
13
+ // prettier-ignore
14
+ // @ts-ignore
15
+ // eslint-disable-next-line max-len
16
+ const __dirname = typeof global.__dirname !== 'undefined' ? global.__dirname : path.dirname(fileURLToPath(import.meta.url));
17
+
11
18
  /**
12
19
  * @param {String} testTitle - Test title
13
20
  *
@@ -33,12 +40,24 @@ const getTestomatIdFromTestTitle = testTitle => {
33
40
  const parseSuite = suiteTitle => {
34
41
  const captures = suiteTitle.match(/@S[\w\d]{8}/);
35
42
  if (captures) {
36
- return captures[1];
43
+ return captures[0];
37
44
  }
38
45
 
39
46
  return null;
40
47
  };
41
48
 
49
+ /**
50
+ * Validates TESTOMATIO_SUITE environment variable format
51
+ * @param {String} suiteId - suite ID to validate
52
+ * @returns {String|null} validated suite ID or null if invalid
53
+ */
54
+ const validateSuiteId = suiteId => {
55
+ if (!suiteId) return null;
56
+
57
+ const match = suiteId.match(SUITE_ID_REGEX);
58
+ return match ? match[0] : null;
59
+ };
60
+
42
61
  const ansiRegExp = () => {
43
62
  const pattern = [
44
63
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -50,7 +69,6 @@ const ansiRegExp = () => {
50
69
 
51
70
  const isValidUrl = s => {
52
71
  try {
53
- // eslint-disable-next-line no-new
54
72
  new URL(s);
55
73
  return true;
56
74
  } catch (err) {
@@ -58,16 +76,25 @@ const isValidUrl = s => {
58
76
  }
59
77
  };
60
78
 
61
- const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/gi;
79
+ const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
62
80
 
63
- const fetchFilesFromStackTrace = (stack = '') => {
81
+ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
64
82
  const files = Array.from(stack.matchAll(fileMatchRegex))
65
83
  .map(f => f[1].trim())
66
- .map(f => (f.startsWith('//') ? f.substring(1) : f));
84
+ .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
85
+ .map(f => {
86
+ // Convert Windows paths to Linux paths for testing purposes
87
+ if (f.match(/^[A-Za-z]:[\\\/]/)) {
88
+ // Convert Windows path to Linux equivalent for test scenarios
89
+ return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
90
+ }
91
+ return f;
92
+ });
67
93
 
68
94
  debug('Found files in stack trace: ', files);
69
95
 
70
96
  return files.filter(f => {
97
+ if (!checkExists) return true;
71
98
  const isFile = fs.existsSync(f);
72
99
  if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
73
100
  return isFile;
@@ -108,7 +135,8 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
108
135
  .join('\n');
109
136
  };
110
137
 
111
- const TEST_ID_REGEX = /@T([\w\d]{8})/;
138
+ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
139
+ export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
112
140
 
113
141
  const fetchIdFromCode = (code, opts = {}) => {
114
142
  const comments = code
@@ -128,12 +156,9 @@ const fetchIdFromCode = (code, opts = {}) => {
128
156
  };
129
157
 
130
158
  const fetchIdFromOutput = output => {
131
- const lines = output
132
- .split('\n')
133
- .map(l => l.trim())
134
- .filter(l => l.startsWith('tid://'));
159
+ const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${TEST_ID_REGEX.source})`);
135
160
 
136
- return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
161
+ return output.match(TID_FULL_PATTERN)?.[2];
137
162
  };
138
163
 
139
164
  const fetchSourceCode = (contents, opts = {}) => {
@@ -154,6 +179,9 @@ const fetchSourceCode = (contents, opts = {}) => {
154
179
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
155
180
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
156
181
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
182
+ } else if (opts.lang === 'csharp') {
183
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
184
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
157
185
  } else {
158
186
  lineIndex = lines.findIndex(l => l.includes(title));
159
187
  }
@@ -300,7 +328,6 @@ const decamelize = text => {
300
328
  * @returns
301
329
  */
302
330
  function removeColorCodes(input) {
303
- // eslint-disable-next-line no-control-regex
304
331
  return input.replace(/\x1b\[[0-9;]*m/g, '');
305
332
  }
306
333
 
@@ -313,7 +340,6 @@ const testRunnerHelper = {
313
340
  try {
314
341
  // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
315
342
  // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
316
- // eslint-disable-next-line no-undef
317
343
  return expect?.getState()?.currentTestName;
318
344
  } catch (e) {
319
345
  return null;
@@ -359,6 +385,12 @@ function formatStep(step, shift = 0) {
359
385
  return lines;
360
386
  }
361
387
 
388
+ export function getPackageVersion() {
389
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
390
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
391
+ return packageJson.version;
392
+ }
393
+
362
394
  export {
363
395
  ansiRegExp,
364
396
  isSameTest,
@@ -380,4 +412,5 @@ export {
380
412
  specificTestInfo,
381
413
  storeRunId,
382
414
  testRunnerHelper,
415
+ validateSuiteId,
383
416
  };
package/src/xmlReader.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  fetchSourceCodeFromStackTrace,
14
14
  fetchIdFromCode,
15
15
  humanize,
16
+ TEST_ID_REGEX,
16
17
  } from './utils/utils.js';
17
18
  import { pipesFactory } from './pipe/index.js';
18
19
  import adapterFactory from './junit-adapter/index.js';
@@ -26,8 +27,15 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
26
27
  const ridRunId = randomUUID();
27
28
 
28
29
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
29
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } =
30
- process.env;
30
+ const {
31
+ TESTOMATIO_RUNGROUP_TITLE,
32
+ TESTOMATIO_SUITE,
33
+ TESTOMATIO_MAX_STACK_TRACE,
34
+ TESTOMATIO_TITLE,
35
+ TESTOMATIO_ENV,
36
+ TESTOMATIO_RUN,
37
+ TESTOMATIO_MARK_DETACHED,
38
+ } = process.env;
31
39
 
32
40
  const options = {
33
41
  ignoreDeclaration: true,
@@ -37,6 +45,8 @@ const options = {
37
45
  parseTagValue: true,
38
46
  };
39
47
 
48
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
49
+
40
50
  const reduceOptions = {};
41
51
 
42
52
  class XmlReader {
@@ -91,7 +101,7 @@ class XmlReader {
91
101
  ];
92
102
 
93
103
  for (const regex of cutRegexes) {
94
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
104
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
95
105
  }
96
106
 
97
107
  const jsonResult = this.parser.parse(xmlData);
@@ -226,7 +236,7 @@ class XmlReader {
226
236
 
227
237
  return {
228
238
  status,
229
- create_tests: true,
239
+ create_tests: !process.env.IGNORE_NEW_TESTS,
230
240
  tests_count: parseInt(counters.total, 10),
231
241
  passed_count: parseInt(counters.passed, 10),
232
242
  skipped_count: parseInt(counters.notExecuted, 10),
@@ -341,6 +351,7 @@ class XmlReader {
341
351
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
342
352
  if (file.endsWith('.js')) this.stats.language = 'js';
343
353
  if (file.endsWith('.ts')) this.stats.language = 'ts';
354
+ if (file.endsWith('.cs')) this.stats.language = 'csharp';
344
355
  }
345
356
 
346
357
  if (!fs.existsSync(file)) {
@@ -394,13 +405,14 @@ class XmlReader {
394
405
  async uploadArtifacts() {
395
406
  for (const test of this.tests.filter(t => !!t.stack)) {
396
407
  let files = [];
397
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
398
- files = [...files, ...fetchFilesFromStackTrace(test.stack)];
408
+ if (!test.files?.length) continue;
409
+
410
+ files = test.files.map(f => (path.isAbsolute(f) ? f : path.join(process.cwd(), f)));
399
411
 
400
412
  if (!files.length) continue;
401
413
 
402
414
  const runId = this.runId || this.store.runId || Date.now().toString();
403
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
415
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
404
416
  console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
405
417
  }
406
418
  }
@@ -460,14 +472,18 @@ function reduceTestCases(prev, item) {
460
472
  }
461
473
 
462
474
  // suite inside test case
463
- if (item['test-suite'] && item['test-suite']['test-case']) testCases.push(...item['test-suite']['test-case']);
475
+ const testCase = item['test-suite']?.['test-case'];
476
+ if (testCase) {
477
+ const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
478
+ testCases.push(...nestedCases);
479
+ }
464
480
 
465
481
  const suiteOutput = item['system-out'] || item.output || item.log || '';
466
482
  const suiteErr = item['system-err'] || item.output || item.log || '';
467
483
  testCases
468
484
  .filter(t => !!t)
469
485
  .forEach(testCaseItem => {
470
- const file = testCaseItem.file || item.filepath || '';
486
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
471
487
 
472
488
  let stack = '';
473
489
  let message = '';
@@ -485,7 +501,7 @@ function reduceTestCases(prev, item) {
485
501
  const preferClassname = reduceOptions.preferClassname || isParametrized;
486
502
 
487
503
  // SpecFlow config
488
- let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
504
+ let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
489
505
  let example = null;
490
506
  const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
491
507
 
@@ -498,19 +514,44 @@ function reduceTestCases(prev, item) {
498
514
  title = title.replace(/\(.*?\)/, '').trim();
499
515
  }
500
516
 
501
- // eslint-disable-next-line
502
517
  stack = `${
503
518
  testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
504
519
  }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
505
- const testId = fetchIdFromOutput(stack);
520
+
521
+ if (!testId) testId = fetchIdFromOutput(stack);
522
+
523
+ if (tags?.length && !testId) {
524
+ testId = tags
525
+ .filter(t => t.startsWith('T'))
526
+ .map(t => `@${t}`)
527
+ .find(t => t.match(TEST_ID_REGEX))
528
+ ?.slice(2);
529
+ }
506
530
 
507
531
  let status = STATUS.PASSED.toString();
508
532
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
509
533
  if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
534
+ if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
535
+ status = testCaseItem.result.toLowerCase();
536
+ }
510
537
 
511
538
  let rid = null;
512
539
  if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
513
540
 
541
+ // Extract attachments
542
+ let files = [];
543
+ if (testCaseItem.attachments) {
544
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
545
+ ? testCaseItem.attachments.attachment
546
+ : [testCaseItem.attachments.attachment];
547
+
548
+ files = attachments.filter(a => a && a.filePath).map(a => a.filePath);
549
+ }
550
+
551
+ // Extract files from stack trace using existing utility
552
+ const stackFiles = fetchFilesFromStackTrace(stack);
553
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
554
+
514
555
  prev.push({
515
556
  rid,
516
557
  file,
@@ -525,7 +566,9 @@ function reduceTestCases(prev, item) {
525
566
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
526
567
  status,
527
568
  title,
569
+ root_suite_id: TESTOMATIO_SUITE,
528
570
  suite_title: suiteTitle,
571
+ files,
529
572
  });
530
573
  });
531
574
  return prev;
@@ -552,11 +595,20 @@ function fetchProperties(item) {
552
595
 
553
596
  if (!item.properties) return {};
554
597
 
555
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
598
+ // Handle both single property and array of properties
599
+ const properties = Array.isArray(item.properties.property)
600
+ ? item.properties.property
601
+ : [item.properties.property].filter(Boolean);
602
+
603
+ const prop = properties.find(p => p.name === 'Description');
556
604
  if (prop) title = prop.value;
557
- [item.properties?.property]
558
- .flat()
559
- .filter(p => p.name === 'Category')
560
- .forEach(p => tags.push(p.value));
561
- return { title, tags };
605
+
606
+ let testId = properties.find(p => p.name === 'ID')?.value;
607
+
608
+ if (testId?.startsWith('@')) testId = testId.slice(1);
609
+ if (testId?.startsWith('T')) testId = testId.slice(1);
610
+
611
+ properties.filter(p => p.name === 'Category').forEach(p => tags.push(p.value));
612
+
613
+ return { title, tags, testId };
562
614
  }