@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.0

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 (62) hide show
  1. package/README.md +3 -2
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +40 -11
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +57 -152
  7. package/lib/data-storage.d.ts +1 -1
  8. package/lib/helpers.d.ts +1 -0
  9. package/lib/helpers.js +4 -0
  10. package/lib/junit-adapter/csharp.d.ts +0 -1
  11. package/lib/junit-adapter/csharp.js +43 -7
  12. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  13. package/lib/junit-adapter/nunit-parser.js +433 -0
  14. package/lib/pipe/bitbucket.js +5 -5
  15. package/lib/pipe/coverage.d.ts +82 -0
  16. package/lib/pipe/coverage.js +373 -0
  17. package/lib/pipe/gitlab.js +4 -4
  18. package/lib/pipe/index.js +2 -0
  19. package/lib/pipe/testomatio.d.ts +3 -2
  20. package/lib/pipe/testomatio.js +44 -18
  21. package/lib/reporter-functions.js +14 -12
  22. package/lib/reporter.d.ts +31 -21
  23. package/lib/reporter.js +40 -5
  24. package/lib/services/artifacts.d.ts +1 -1
  25. package/lib/services/key-values.d.ts +1 -1
  26. package/lib/services/links.d.ts +1 -1
  27. package/lib/services/logger.d.ts +1 -1
  28. package/lib/uploader.js +4 -0
  29. package/lib/utils/log-formatter.d.ts +28 -0
  30. package/lib/utils/log-formatter.js +127 -0
  31. package/lib/utils/pipe_utils.d.ts +15 -0
  32. package/lib/utils/pipe_utils.js +44 -2
  33. package/lib/utils/utils.d.ts +6 -0
  34. package/lib/utils/utils.js +260 -25
  35. package/lib/xmlReader.d.ts +32 -26
  36. package/lib/xmlReader.js +121 -52
  37. package/package.json +12 -7
  38. package/src/adapter/codecept.js +19 -19
  39. package/src/adapter/mocha.js +1 -1
  40. package/src/adapter/playwright.js +2 -2
  41. package/src/bin/cli.js +51 -13
  42. package/src/bin/reportXml.js +5 -2
  43. package/src/client.js +69 -130
  44. package/src/helpers.js +1 -0
  45. package/src/junit-adapter/csharp.js +48 -6
  46. package/src/junit-adapter/nunit-parser.js +474 -0
  47. package/src/pipe/bitbucket.js +5 -5
  48. package/src/pipe/coverage.js +440 -0
  49. package/src/pipe/debug.js +1 -2
  50. package/src/pipe/gitlab.js +4 -4
  51. package/src/pipe/index.js +2 -0
  52. package/src/pipe/testomatio.js +109 -85
  53. package/src/reporter-functions.js +15 -12
  54. package/src/reporter.js +6 -4
  55. package/src/services/links.js +1 -1
  56. package/src/uploader.js +5 -0
  57. package/src/utils/log-formatter.js +113 -0
  58. package/src/utils/pipe_utils.js +52 -3
  59. package/src/utils/utils.js +277 -22
  60. package/src/xmlReader.js +144 -46
  61. package/types/types.d.ts +364 -0
  62. package/types/vitest.types.d.ts +93 -0
package/src/client.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import createDebugMessages from 'debug';
2
- import createCallsiteRecord from 'callsite-record';
3
- import { minimatch } from 'minimatch';
4
2
  import fs from 'fs';
5
3
  import pc from 'picocolors';
6
- import { randomUUID } from 'crypto';
7
4
  import { APP_PREFIX, STATUS } from './constants.js';
8
5
  import { pipesFactory } from './pipe/index.js';
9
6
  import { glob } from 'glob';
10
- import path, { sep } from 'path';
7
+ import path from 'path';
11
8
  import { fileURLToPath } from 'node:url';
12
9
  import { S3Uploader } from './uploader.js';
13
- import { formatStep, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
10
+ import { readLatestRunId, storeRunId, validateSuiteId, transformEnvVarToBoolean } from './utils/utils.js';
14
11
  import { filesize as prettyBytes } from 'filesize';
12
+ import { formatLogs, formatError, stripColors } from './utils/log-formatter.js';
15
13
 
16
14
  const debug = createDebugMessages('@testomatio/reporter:client');
17
15
 
@@ -67,35 +65,43 @@ class Client {
67
65
  * array containing the prepared execution list,
68
66
  * or resolves to undefined if no valid results are found or if all pipes are disabled.
69
67
  */
68
+
70
69
  async prepareRun(params) {
71
- this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
72
70
  const { pipe, pipeOptions } = params;
71
+
72
+ // ❗ Validation: pipe is required
73
+ if (!pipe || !pipeOptions) {
74
+ console.warn(`❗ No valid pipe found in filter cmd. Expected format: <pipe>:<options>
75
+ Examples:
76
+ --filter "testomatio:tag-name=frontend"
77
+ --filter "coverage:file=coverage.yml"
78
+ --filter-list "coverage:file=coverage.yml"
79
+ Received: "${params}"`);
80
+ return;
81
+ }
82
+
83
+ this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
84
+
73
85
  // all pipes disabled, skipping
74
86
  if (!this.pipes.some(p => p.isEnabled)) {
75
87
  return Promise.resolve();
76
88
  }
77
89
 
78
90
  try {
79
- const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
91
+ const p = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
92
+ // const p = this.pipes.find(p => p.id === `${pipe.toLowerCase()}`); TODO: as future updates
80
93
 
81
- if (!filterPipe?.isEnabled) {
82
- // TODO:for the future for the another pipes
94
+ if (!p?.isEnabled) {
83
95
  console.warn(
84
96
  APP_PREFIX,
85
- `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
97
+ "🚫 No active pipes were found in the system. Execution aborted!"
86
98
  );
87
99
  return;
88
100
  }
89
101
 
90
- const results = await Promise.all(
91
- this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
92
- );
93
-
94
- const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
95
-
96
- if (!result || result.length === 0) {
97
- return;
98
- }
102
+ // Run only the selected pipe
103
+ const rawResult = await p.prepareRun(pipeOptions);
104
+ const result = Array.isArray(rawResult) ? rawResult : [];
99
105
 
100
106
  debug('Execution tests list', result);
101
107
 
@@ -110,7 +116,7 @@ class Client {
110
116
  *
111
117
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
112
118
  */
113
- async createRun(params) {
119
+ async createRun(params = {}) {
114
120
  if (!this.pipes || !this.pipes.length)
115
121
  this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
116
122
  debug('Creating run...');
@@ -118,7 +124,7 @@ class Client {
118
124
  if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
119
125
 
120
126
  this.queue = this.queue
121
- .then(() => Promise.all(this.pipes.map(p => p.createRun())))
127
+ .then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
122
128
  .catch(err => console.log(APP_PREFIX, err))
123
129
  .then(() => {
124
130
  const runId = this.pipeStore?.runId;
@@ -139,19 +145,6 @@ class Client {
139
145
  * @returns {Promise<PipeResult[]>}
140
146
  */
141
147
  async addTestRun(status, testData) {
142
- if (!this.pipes || !this.pipes.length)
143
- this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
144
-
145
- // all pipes disabled, skipping
146
- if (!this.pipes?.filter(p => p.isEnabled).length) return [];
147
-
148
- if (isTestShouldBeExculedFromReport(testData)) return [];
149
-
150
- if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
151
- debug('Skipping test from report', testData?.title);
152
- return []; // do not log skipped tests
153
- }
154
-
155
148
  if (!testData)
156
149
  testData = {
157
150
  title: 'Unknown test',
@@ -166,18 +159,19 @@ class Client {
166
159
  /**
167
160
  * @type {TestData}
168
161
  */
162
+ const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
163
+ let steps = originalSteps;
164
+
165
+ const uploadedFiles = [];
166
+ const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
167
+
169
168
  const {
170
- rid,
171
- error = null,
172
169
  time = 0,
173
170
  example = null,
174
171
  files = [],
175
172
  filesBuffers = [],
176
- steps,
177
173
  code = null,
178
- title,
179
174
  file,
180
- suite_title,
181
175
  suite_id,
182
176
  test_id,
183
177
  timestamp,
@@ -188,7 +182,6 @@ class Client {
188
182
  } = testData;
189
183
  let { message = '', meta = {} } = testData;
190
184
 
191
- // stringify meta values and limit keys and values length to 255
192
185
  meta = Object.entries(meta)
193
186
  .filter(([, value]) => value !== null && value !== undefined)
194
187
  .reduce((acc, [key, value]) => {
@@ -196,22 +189,46 @@ class Client {
196
189
  return acc;
197
190
  }, {});
198
191
 
199
- // Get links from storage using the test context
200
192
  const testContext = suite_title ? `${suite_title} ${title}` : title;
201
193
 
202
194
  let errorFormatted = '';
203
195
  if (error) {
204
- errorFormatted += this.formatError(error) || '';
196
+ errorFormatted += formatError(error) || '';
205
197
  message = error?.message;
206
198
  }
207
199
 
208
- // Attach logs
209
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
200
+ let fullLogs = formatLogs({ error: errorFormatted, steps, logs: testData.logs });
210
201
 
211
- // add artifacts
212
- if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
202
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
203
+ uploadedFiles.push(
204
+ this.uploader.uploadFileAsBuffer(Buffer.from(stripColors(fullLogs), 'utf8'), [
205
+ this.runId,
206
+ rid,
207
+ `logs_${+new Date()}.log`,
208
+ ]),
209
+ );
210
+ fullLogs = '';
211
+ steps = null;
212
+ }
213
213
 
214
- const uploadedFiles = [];
214
+ if (!this.pipes || !this.pipes.length)
215
+ this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
216
+
217
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
218
+ if (uploadedFiles.length > 0) {
219
+ await Promise.all(uploadedFiles);
220
+ }
221
+ return [];
222
+ }
223
+
224
+ if (isTestShouldBeExcludedFromReport(testData)) return [];
225
+
226
+ if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
227
+ debug('Skipping test from report', testData?.title);
228
+ return [];
229
+ }
230
+
231
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
215
232
 
216
233
  for (let f of files) {
217
234
  if (!f) continue; // f === null
@@ -308,7 +325,7 @@ class Client {
308
325
  const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
309
326
  relativePath: file.path.replace(process.cwd(), ''),
310
327
  link: file.link,
311
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
328
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
312
329
  }));
313
330
 
314
331
  uploadedArtifacts.forEach(upload => {
@@ -330,7 +347,7 @@ class Client {
330
347
  );
331
348
  const failedUploads = this.uploader.failedUploads.map(file => ({
332
349
  relativePath: file.path.replace(process.cwd(), ''),
333
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
350
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
334
351
  }));
335
352
 
336
353
  const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
@@ -380,84 +397,6 @@ class Client {
380
397
 
381
398
  return this.queue;
382
399
  }
383
-
384
- /**
385
- * Returns the formatted stack including the stack trace, steps, and logs.
386
- * @returns {string}
387
- */
388
- formatLogs({ error, steps, logs }) {
389
- error = error?.trim();
390
- logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
391
-
392
- if (Array.isArray(steps)) {
393
- steps = steps
394
- .map(step => formatStep(step))
395
- .flat()
396
- .join('\n');
397
- }
398
-
399
- let testLogs = '';
400
- if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
401
- if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
402
- if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
403
- return testLogs;
404
- }
405
-
406
- formatError(error, message) {
407
- if (!message) message = error.message;
408
- if (error.inspect) message = error.inspect() || '';
409
-
410
- let stack = '';
411
- if (error.name) stack += `${pc.red(error.name)}`;
412
- if (error.operator) stack += ` (${pc.red(error.operator)})`;
413
- // add new line if something was added to stack
414
- if (stack) stack += ': ';
415
-
416
- stack += `${message}\n`;
417
-
418
- if (error.diff) {
419
- // diff for vitest
420
- stack += error.diff;
421
- stack += '\n\n';
422
- } else if (error.actual && error.expected && error.actual !== error.expected) {
423
- // diffs for mocha, cypress, codeceptjs style
424
- stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
425
- stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
426
- stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
427
- stack += '\n\n';
428
- }
429
-
430
- const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
431
-
432
- try {
433
- let hasFrame = false;
434
- const record = createCallsiteRecord({
435
- forError: error,
436
- isCallsiteFrame: frame => {
437
- if (customFilter && minimatch(frame.fileName, customFilter)) return false;
438
- if (hasFrame) return false;
439
- if (isNotInternalFrame(frame)) hasFrame = true;
440
- return hasFrame;
441
- },
442
- });
443
- // @ts-ignore
444
- if (record && !record.filename.startsWith('http')) {
445
- stack += record.renderSync({ stackFilter: isNotInternalFrame });
446
- }
447
- return stack;
448
- } catch (e) {
449
- console.log(e);
450
- }
451
- }
452
- }
453
-
454
- function isNotInternalFrame(frame) {
455
- return (
456
- frame.getFileName() &&
457
- frame.getFileName().includes(sep) &&
458
- !frame.getFileName().includes('node_modules') &&
459
- !frame.getFileName().includes('internal')
460
- );
461
400
  }
462
401
 
463
402
  /**
@@ -465,7 +404,7 @@ function isNotInternalFrame(frame) {
465
404
  * @param {TestData} testData
466
405
  * @returns boolean
467
406
  */
468
- function isTestShouldBeExculedFromReport(testData) {
407
+ function isTestShouldBeExcludedFromReport(testData) {
469
408
  // const fileName = path.basename(test.location?.file || '');
470
409
  const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
471
410
  if (!globExcludeFilesPattern) return false;
@@ -475,12 +414,12 @@ function isTestShouldBeExculedFromReport(testData) {
475
414
  return false;
476
415
  }
477
416
 
478
- const excludeParretnsList = globExcludeFilesPattern.split(';');
417
+ const excludePatternsList = globExcludeFilesPattern.split(';');
479
418
 
480
419
  // as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
481
420
  if (!listOfTestFilesToExcludeFromReport) {
482
421
  // list of files with relative paths
483
- listOfTestFilesToExcludeFromReport = glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
422
+ listOfTestFilesToExcludeFromReport = glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
484
423
  debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
485
424
  }
486
425
 
package/src/helpers.js ADDED
@@ -0,0 +1 @@
1
+ export const isPlaywright = Boolean(process.env.PLAYWRIGHT_TEST || process.env.PLAYWRIGHT);
@@ -3,18 +3,53 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- const title = t.title.replace(/\(.*?\)/, '').trim();
7
- const example = t.title.match(/\((.*?)\)/);
8
- if (example) t.example = { ...example[1].split(',') };
6
+ // Extract example from title if not already present
7
+ if (!t.example) {
8
+ const exampleMatch = t.title.match(/\((.*?)\)/);
9
+ if (exampleMatch) {
10
+ // Extract parameters as object with numeric keys for API
11
+ const params = exampleMatch[1]
12
+ .split(',')
13
+ .map(param => param.trim())
14
+ .filter(param => param !== '');
15
+ t.example = {};
16
+ params.forEach((param, index) => {
17
+ t.example[index] = param;
18
+ });
19
+ }
20
+ }
21
+
22
+ // Remove parameters from title to avoid duplicates in Test Suite
23
+ // The example field will be used for grouping on import
24
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
25
+
9
26
  const suite = t.suite_title.split('.');
10
27
  t.suite_title = suite.pop();
11
28
  t.file = namespaceToFileName(t.file);
12
- t.title = title.trim();
13
29
  return t;
14
30
  }
15
31
 
16
32
  getFilePath(t) {
17
- const fileName = namespaceToFileName(t.file);
33
+ if (!t.file) return null;
34
+
35
+ // Normalize path separators for cross-platform compatibility
36
+ let filePath = t.file.replace(/\\/g, '/');
37
+
38
+ // If file already has .cs extension, use it directly
39
+ if (filePath.endsWith('.cs')) {
40
+ // Make relative path if it's absolute
41
+ if (path.isAbsolute(filePath)) {
42
+ // Try to find project-relative path
43
+ const cwd = process.cwd().replace(/\\/g, '/');
44
+ if (filePath.startsWith(cwd)) {
45
+ filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
46
+ }
47
+ }
48
+ return filePath;
49
+ }
50
+
51
+ // Convert namespace path to file path
52
+ const fileName = namespaceToFileName(filePath);
18
53
  return fileName;
19
54
  }
20
55
  }
@@ -22,7 +57,14 @@ class CSharpAdapter extends Adapter {
22
57
  export default CSharpAdapter;
23
58
 
24
59
  function namespaceToFileName(fileName) {
60
+ if (!fileName) return '';
61
+
62
+ // If already a .cs file path, clean it up
63
+ if (fileName.endsWith('.cs')) {
64
+ return fileName.replace(/\\/g, '/');
65
+ }
66
+
25
67
  const fileParts = fileName.split('.');
26
68
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
27
- return `${fileParts.join(path.sep)}.cs`;
69
+ return `${fileParts.join('/')}.cs`;
28
70
  }