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

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 (45) hide show
  1. package/README.md +2 -1
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +14 -4
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +39 -142
  7. package/lib/junit-adapter/csharp.d.ts +0 -1
  8. package/lib/junit-adapter/csharp.js +43 -7
  9. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  10. package/lib/junit-adapter/nunit-parser.js +433 -0
  11. package/lib/pipe/bitbucket.js +5 -5
  12. package/lib/pipe/gitlab.js +4 -4
  13. package/lib/pipe/testomatio.d.ts +2 -1
  14. package/lib/pipe/testomatio.js +19 -14
  15. package/lib/reporter-functions.js +1 -3
  16. package/lib/reporter.d.ts +19 -9
  17. package/lib/reporter.js +40 -5
  18. package/lib/uploader.js +4 -0
  19. package/lib/utils/log-formatter.d.ts +28 -0
  20. package/lib/utils/log-formatter.js +127 -0
  21. package/lib/utils/utils.js +189 -24
  22. package/lib/xmlReader.d.ts +32 -26
  23. package/lib/xmlReader.js +121 -52
  24. package/package.json +8 -4
  25. package/src/adapter/codecept.js +19 -19
  26. package/src/adapter/mocha.js +1 -1
  27. package/src/adapter/playwright.js +2 -2
  28. package/src/bin/cli.js +16 -4
  29. package/src/bin/reportXml.js +5 -2
  30. package/src/client.js +47 -116
  31. package/src/junit-adapter/csharp.js +48 -6
  32. package/src/junit-adapter/nunit-parser.js +474 -0
  33. package/src/pipe/bitbucket.js +5 -5
  34. package/src/pipe/debug.js +1 -2
  35. package/src/pipe/gitlab.js +4 -4
  36. package/src/pipe/testomatio.js +75 -80
  37. package/src/reporter-functions.js +2 -3
  38. package/src/reporter.js +6 -4
  39. package/src/services/links.js +1 -1
  40. package/src/uploader.js +5 -0
  41. package/src/utils/log-formatter.js +113 -0
  42. package/src/utils/utils.js +202 -22
  43. package/src/xmlReader.js +144 -46
  44. package/types/types.d.ts +364 -0
  45. 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
 
@@ -110,7 +108,7 @@ class Client {
110
108
  *
111
109
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
112
110
  */
113
- async createRun(params) {
111
+ async createRun(params = {}) {
114
112
  if (!this.pipes || !this.pipes.length)
115
113
  this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
116
114
  debug('Creating run...');
@@ -118,7 +116,7 @@ class Client {
118
116
  if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
119
117
 
120
118
  this.queue = this.queue
121
- .then(() => Promise.all(this.pipes.map(p => p.createRun())))
119
+ .then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
122
120
  .catch(err => console.log(APP_PREFIX, err))
123
121
  .then(() => {
124
122
  const runId = this.pipeStore?.runId;
@@ -139,19 +137,6 @@ class Client {
139
137
  * @returns {Promise<PipeResult[]>}
140
138
  */
141
139
  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
140
  if (!testData)
156
141
  testData = {
157
142
  title: 'Unknown test',
@@ -166,18 +151,19 @@ class Client {
166
151
  /**
167
152
  * @type {TestData}
168
153
  */
154
+ const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
155
+ let steps = originalSteps;
156
+
157
+ const uploadedFiles = [];
158
+ const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
159
+
169
160
  const {
170
- rid,
171
- error = null,
172
161
  time = 0,
173
162
  example = null,
174
163
  files = [],
175
164
  filesBuffers = [],
176
- steps,
177
165
  code = null,
178
- title,
179
166
  file,
180
- suite_title,
181
167
  suite_id,
182
168
  test_id,
183
169
  timestamp,
@@ -188,7 +174,6 @@ class Client {
188
174
  } = testData;
189
175
  let { message = '', meta = {} } = testData;
190
176
 
191
- // stringify meta values and limit keys and values length to 255
192
177
  meta = Object.entries(meta)
193
178
  .filter(([, value]) => value !== null && value !== undefined)
194
179
  .reduce((acc, [key, value]) => {
@@ -196,22 +181,46 @@ class Client {
196
181
  return acc;
197
182
  }, {});
198
183
 
199
- // Get links from storage using the test context
200
184
  const testContext = suite_title ? `${suite_title} ${title}` : title;
201
185
 
202
186
  let errorFormatted = '';
203
187
  if (error) {
204
- errorFormatted += this.formatError(error) || '';
188
+ errorFormatted += formatError(error) || '';
205
189
  message = error?.message;
206
190
  }
207
191
 
208
- // Attach logs
209
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
192
+ let fullLogs = formatLogs({ error: errorFormatted, steps, logs: testData.logs });
210
193
 
211
- // add artifacts
212
- if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
194
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
195
+ uploadedFiles.push(
196
+ this.uploader.uploadFileAsBuffer(Buffer.from(stripColors(fullLogs), 'utf8'), [
197
+ this.runId,
198
+ rid,
199
+ `logs_${+new Date()}.log`,
200
+ ]),
201
+ );
202
+ fullLogs = '';
203
+ steps = null;
204
+ }
213
205
 
214
- const uploadedFiles = [];
206
+ if (!this.pipes || !this.pipes.length)
207
+ this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
208
+
209
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
210
+ if (uploadedFiles.length > 0) {
211
+ await Promise.all(uploadedFiles);
212
+ }
213
+ return [];
214
+ }
215
+
216
+ if (isTestShouldBeExcludedFromReport(testData)) return [];
217
+
218
+ if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
219
+ debug('Skipping test from report', testData?.title);
220
+ return [];
221
+ }
222
+
223
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
215
224
 
216
225
  for (let f of files) {
217
226
  if (!f) continue; // f === null
@@ -308,7 +317,7 @@ class Client {
308
317
  const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
309
318
  relativePath: file.path.replace(process.cwd(), ''),
310
319
  link: file.link,
311
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
320
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
312
321
  }));
313
322
 
314
323
  uploadedArtifacts.forEach(upload => {
@@ -330,7 +339,7 @@ class Client {
330
339
  );
331
340
  const failedUploads = this.uploader.failedUploads.map(file => ({
332
341
  relativePath: file.path.replace(process.cwd(), ''),
333
- sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
342
+ sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
334
343
  }));
335
344
 
336
345
  const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
@@ -380,84 +389,6 @@ class Client {
380
389
 
381
390
  return this.queue;
382
391
  }
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
392
  }
462
393
 
463
394
  /**
@@ -465,7 +396,7 @@ function isNotInternalFrame(frame) {
465
396
  * @param {TestData} testData
466
397
  * @returns boolean
467
398
  */
468
- function isTestShouldBeExculedFromReport(testData) {
399
+ function isTestShouldBeExcludedFromReport(testData) {
469
400
  // const fileName = path.basename(test.location?.file || '');
470
401
  const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
471
402
  if (!globExcludeFilesPattern) return false;
@@ -475,12 +406,12 @@ function isTestShouldBeExculedFromReport(testData) {
475
406
  return false;
476
407
  }
477
408
 
478
- const excludeParretnsList = globExcludeFilesPattern.split(';');
409
+ const excludePatternsList = globExcludeFilesPattern.split(';');
479
410
 
480
411
  // as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
481
412
  if (!listOfTestFilesToExcludeFromReport) {
482
413
  // list of files with relative paths
483
- listOfTestFilesToExcludeFromReport = glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
414
+ listOfTestFilesToExcludeFromReport = glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
484
415
  debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
485
416
  }
486
417
 
@@ -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
  }