@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.2-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 +31 -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 +259 -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 +57 -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 +13 -5
  57. package/src/bin/cli.js +78 -7
  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 +28 -5
  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 +112 -107
  70. package/src/replay.js +268 -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 +67 -15
  80. package/src/xmlReader.js +73 -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;
@@ -327,20 +353,38 @@ function storeRunId(runId) {
327
353
  fs.writeFileSync(filePath, runId);
328
354
  }
329
355
 
356
+ /**
357
+ *
358
+ * @returns {String|null} latest run ID
359
+ */
330
360
  function readLatestRunId() {
331
361
  try {
332
362
  const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
333
363
  const stats = fs.statSync(filePath);
334
364
  const diff = +new Date() - +stats.mtime;
335
365
  const diffHours = diff / 1000 / 60 / 60;
336
- if (diffHours > 1) return;
366
+ if (diffHours > 1) return null;
337
367
 
338
- return fs.readFileSync(filePath)?.toString()?.trim();
368
+ return fs.readFileSync(filePath)?.toString()?.trim() ?? null;
339
369
  } catch (e) {
370
+ console.warn('Could not read latest run ID from file: ', e);
340
371
  return null;
341
372
  }
342
373
  }
343
374
 
375
+ function cleanLatestRunId() {
376
+ try {
377
+ const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
378
+ const runId = readLatestRunId();
379
+ if (fs.existsSync(filePath)) {
380
+ fs.unlinkSync(filePath);
381
+ }
382
+ debug(`Cleaned latest run ID (${runId}) file`, filePath);
383
+ } catch (e) {
384
+ console.warn('Could not clean latest run ID file: ', e);
385
+ }
386
+ }
387
+
344
388
  function formatStep(step, shift = 0) {
345
389
  const prefix = ' '.repeat(shift);
346
390
 
@@ -359,8 +403,15 @@ function formatStep(step, shift = 0) {
359
403
  return lines;
360
404
  }
361
405
 
406
+ export function getPackageVersion() {
407
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
408
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
409
+ return packageJson.version;
410
+ }
411
+
362
412
  export {
363
413
  ansiRegExp,
414
+ cleanLatestRunId,
364
415
  isSameTest,
365
416
  fetchSourceCode,
366
417
  fetchSourceCodeFromStackTrace,
@@ -380,4 +431,5 @@ export {
380
431
  specificTestInfo,
381
432
  storeRunId,
382
433
  testRunnerHelper,
434
+ validateSuiteId,
383
435
  };
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);
@@ -207,6 +217,7 @@ class XmlReader {
207
217
  if (test.example) r.example = test.example;
208
218
  if (test.file) r.file = test.file;
209
219
  r.create = true;
220
+ r.overwrite = true;
210
221
  if (r.status === 'Passed') r.status = STATUS.PASSED;
211
222
  if (r.status === 'Failed') r.status = STATUS.FAILED;
212
223
  if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
@@ -226,7 +237,7 @@ class XmlReader {
226
237
 
227
238
  return {
228
239
  status,
229
- create_tests: true,
240
+ create_tests: !process.env.IGNORE_NEW_TESTS,
230
241
  tests_count: parseInt(counters.total, 10),
231
242
  passed_count: parseInt(counters.passed, 10),
232
243
  skipped_count: parseInt(counters.notExecuted, 10),
@@ -283,6 +294,7 @@ class XmlReader {
283
294
  title,
284
295
  suite_title,
285
296
  run_time,
297
+ retry: false,
286
298
  });
287
299
  });
288
300
  });
@@ -341,6 +353,7 @@ class XmlReader {
341
353
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
342
354
  if (file.endsWith('.js')) this.stats.language = 'js';
343
355
  if (file.endsWith('.ts')) this.stats.language = 'ts';
356
+ if (file.endsWith('.cs')) this.stats.language = 'csharp';
344
357
  }
345
358
 
346
359
  if (!fs.existsSync(file)) {
@@ -394,13 +407,14 @@ class XmlReader {
394
407
  async uploadArtifacts() {
395
408
  for (const test of this.tests.filter(t => !!t.stack)) {
396
409
  let files = [];
397
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
398
- files = [...files, ...fetchFilesFromStackTrace(test.stack)];
410
+ if (!test.files?.length) continue;
411
+
412
+ files = test.files.map(f => (path.isAbsolute(f) ? f : path.join(process.cwd(), f)));
399
413
 
400
414
  if (!files.length) continue;
401
415
 
402
416
  const runId = this.runId || this.store.runId || Date.now().toString();
403
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
417
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
404
418
  console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
405
419
  }
406
420
  }
@@ -460,14 +474,18 @@ function reduceTestCases(prev, item) {
460
474
  }
461
475
 
462
476
  // suite inside test case
463
- if (item['test-suite'] && item['test-suite']['test-case']) testCases.push(...item['test-suite']['test-case']);
477
+ const testCase = item['test-suite']?.['test-case'];
478
+ if (testCase) {
479
+ const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
480
+ testCases.push(...nestedCases);
481
+ }
464
482
 
465
483
  const suiteOutput = item['system-out'] || item.output || item.log || '';
466
484
  const suiteErr = item['system-err'] || item.output || item.log || '';
467
485
  testCases
468
486
  .filter(t => !!t)
469
487
  .forEach(testCaseItem => {
470
- const file = testCaseItem.file || item.filepath || '';
488
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
471
489
 
472
490
  let stack = '';
473
491
  let message = '';
@@ -485,7 +503,7 @@ function reduceTestCases(prev, item) {
485
503
  const preferClassname = reduceOptions.preferClassname || isParametrized;
486
504
 
487
505
  // SpecFlow config
488
- let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
506
+ let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
489
507
  let example = null;
490
508
  const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
491
509
 
@@ -498,19 +516,44 @@ function reduceTestCases(prev, item) {
498
516
  title = title.replace(/\(.*?\)/, '').trim();
499
517
  }
500
518
 
501
- // eslint-disable-next-line
502
519
  stack = `${
503
520
  testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
504
521
  }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
505
- const testId = fetchIdFromOutput(stack);
522
+
523
+ if (!testId) testId = fetchIdFromOutput(stack);
524
+
525
+ if (tags?.length && !testId) {
526
+ testId = tags
527
+ .filter(t => t.startsWith('T'))
528
+ .map(t => `@${t}`)
529
+ .find(t => t.match(TEST_ID_REGEX))
530
+ ?.slice(2);
531
+ }
506
532
 
507
533
  let status = STATUS.PASSED.toString();
508
534
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
509
535
  if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
536
+ if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
537
+ status = testCaseItem.result.toLowerCase();
538
+ }
510
539
 
511
540
  let rid = null;
512
541
  if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
513
542
 
543
+ // Extract attachments
544
+ let files = [];
545
+ if (testCaseItem.attachments) {
546
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
547
+ ? testCaseItem.attachments.attachment
548
+ : [testCaseItem.attachments.attachment];
549
+
550
+ files = attachments.filter(a => a && a.filePath).map(a => a.filePath);
551
+ }
552
+
553
+ // Extract files from stack trace using existing utility
554
+ const stackFiles = fetchFilesFromStackTrace(stack);
555
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
556
+
514
557
  prev.push({
515
558
  rid,
516
559
  file,
@@ -525,7 +568,10 @@ function reduceTestCases(prev, item) {
525
568
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
526
569
  status,
527
570
  title,
571
+ root_suite_id: TESTOMATIO_SUITE,
528
572
  suite_title: suiteTitle,
573
+ files,
574
+ retry: false,
529
575
  });
530
576
  });
531
577
  return prev;
@@ -552,11 +598,20 @@ function fetchProperties(item) {
552
598
 
553
599
  if (!item.properties) return {};
554
600
 
555
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
601
+ // Handle both single property and array of properties
602
+ const properties = Array.isArray(item.properties.property)
603
+ ? item.properties.property
604
+ : [item.properties.property].filter(Boolean);
605
+
606
+ const prop = properties.find(p => p.name === 'Description');
556
607
  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 };
608
+
609
+ let testId = properties.find(p => p.name === 'ID')?.value;
610
+
611
+ if (testId?.startsWith('@')) testId = testId.slice(1);
612
+ if (testId?.startsWith('T')) testId = testId.slice(1);
613
+
614
+ properties.filter(p => p.name === 'Category').forEach(p => tags.push(p.value));
615
+
616
+ return { title, tags, testId };
562
617
  }