@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/README.md CHANGED
@@ -13,7 +13,7 @@ Testomat.io Reporter (this npm package) supports:
13
13
  - πŸ”Ž [Stack traces](./docs/stacktrace.md) and error messages
14
14
  - πŸ™ [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
15
15
  - πŸš… Realtime reports
16
- - πŸ—ƒοΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
16
+ - πŸ—ƒοΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
17
17
  - πŸšΆβ€β™€οΈ Steps _(work in progress)_
18
18
  - πŸ“„ [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
19
19
  - ☁️ Custom properties and metadata _(work in progress)_
@@ -129,6 +129,7 @@ Bring this reporter on CI and never lose test results again!
129
129
  - [CSV](./docs/pipes/csv.md)
130
130
  - [HTML report](./docs/pipes/html.md)
131
131
  - [Bitbucket](./docs/pipes/bitbucket.md)
132
+ - πŸ”— [Linking Tests](./docs/linking-tests.md)
132
133
  - πŸ““ [JUnit](./docs/junit.md)
133
134
  - πŸ—„οΈ [Artifacts](./docs/artifacts.md)
134
135
  - πŸ”‚ [Workflows](./docs/workflows.md)
@@ -20,11 +20,14 @@ if (!global.codeceptjs) {
20
20
  }
21
21
  // @ts-ignore
22
22
  const { event, recorder, codecept, output } = global.codeceptjs;
23
- const [, MAJOR_VERSION, MINOR_VERSION] = codecept.version().match(/(\d+)\.(\d+)/).map(Number);
23
+ const [, MAJOR_VERSION, MINOR_VERSION] = codecept
24
+ .version()
25
+ .match(/(\d+)\.(\d+)/)
26
+ .map(Number);
24
27
  // Constants for hook execution order
25
28
  const HOOK_EXECUTION_ORDER = {
26
29
  PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
27
- POST_TEST: ['AfterHook', 'AfterSuiteHook']
30
+ POST_TEST: ['AfterHook', 'AfterSuiteHook'],
28
31
  };
29
32
  // codeceptjs workers are self-contained
30
33
  data_storage_js_1.dataStorage.isFileStorage = false;
@@ -92,7 +95,7 @@ function CodeceptReporter(config) {
92
95
  global.testomatioDataStore = {};
93
96
  });
94
97
  // Hook event listeners
95
- event.dispatcher.on(event.hook.started, (hook) => {
98
+ event.dispatcher.on(event.hook.started, hook => {
96
99
  output.stepShift = 2;
97
100
  currentHook = hook.name;
98
101
  let title = hook.hookName;
@@ -281,7 +284,7 @@ function captureHookStep(step, currentHook, hookSteps) {
281
284
  status: step.status,
282
285
  startTime,
283
286
  endTime,
284
- helperMethod: step.helperMethod
287
+ helperMethod: step.helperMethod,
285
288
  });
286
289
  hookSteps.set(currentHook, hookStepsArray);
287
290
  }
@@ -369,7 +372,7 @@ function createSectionStep(metaStep) {
369
372
  category: 'user',
370
373
  title: metaStep.toString(), // Use built-in toString method
371
374
  duration: metaStep.duration || 0, // Use built-in duration
372
- steps: []
375
+ steps: [],
373
376
  };
374
377
  }
375
378
  function createHookSection(hookName, steps) {
@@ -379,7 +382,7 @@ function createHookSection(hookName, steps) {
379
382
  category: 'hook',
380
383
  title: formatHookName(hookName),
381
384
  duration: 0,
382
- steps: []
385
+ steps: [],
383
386
  };
384
387
  for (const step of steps) {
385
388
  const formattedStep = formatHookStep(step);
@@ -403,13 +406,13 @@ function formatCodeceptStep(step) {
403
406
  const formattedStep = {
404
407
  category,
405
408
  title,
406
- duration
409
+ duration,
407
410
  };
408
411
  // Add error if step failed
409
412
  if (step.status === 'failed' && step.err) {
410
413
  formattedStep.error = {
411
414
  message: step.err.message || 'Step failed',
412
- stack: step.err.stack || ''
415
+ stack: step.err.stack || '',
413
416
  };
414
417
  }
415
418
  return formattedStep;
@@ -430,7 +433,7 @@ function formatHookStep(step) {
430
433
  return {
431
434
  category: 'hook',
432
435
  title,
433
- duration: step.duration || 0
436
+ duration: step.duration || 0,
434
437
  };
435
438
  }
436
439
  module.exports = CodeceptReporter;
package/lib/bin/cli.js CHANGED
@@ -37,12 +37,17 @@ program
37
37
  program
38
38
  .command('start')
39
39
  .description('Start a new run and return its ID')
40
- .action(async () => {
40
+ .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
41
+ .action(async (opts) => {
41
42
  (0, utils_js_1.cleanLatestRunId)();
42
43
  console.log('Starting a new Run on Testomat.io...');
43
44
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
44
45
  const client = new client_js_1.default({ apiKey });
45
- client.createRun().then(() => {
46
+ const createRunParams = {};
47
+ if (opts.kind) {
48
+ createRunParams.kind = opts.kind;
49
+ }
50
+ client.createRun(createRunParams).then(() => {
46
51
  console.log(process.env.runId);
47
52
  process.exit(0);
48
53
  });
@@ -70,6 +75,7 @@ program
70
75
  .description('Run tests with the specified command')
71
76
  .argument('<command>', 'Test runner command')
72
77
  .option('--filter <filter>', 'Additional execution filter')
78
+ .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
73
79
  .action(async (command, opts) => {
74
80
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
75
81
  const title = process.env.TESTOMATIO_TITLE;
@@ -108,8 +114,12 @@ program
108
114
  process.exit(code);
109
115
  });
110
116
  };
117
+ const createRunParams = {};
118
+ if (opts.kind) {
119
+ createRunParams.kind = opts.kind;
120
+ }
111
121
  if (apiKey) {
112
- await client.createRun().then(runTests);
122
+ await client.createRun(createRunParams).then(runTests);
113
123
  }
114
124
  else {
115
125
  await runTests();
@@ -145,7 +155,7 @@ program
145
155
  .option('--lang <lang>', 'Language used (python, ruby, java)')
146
156
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
147
157
  .action(async (pattern, opts) => {
148
- if (!pattern.endsWith('.xml')) {
158
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
149
159
  pattern += '.xml';
150
160
  }
151
161
  let { javaTests, lang } = opts;
@@ -25,7 +25,7 @@ program
25
25
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
26
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
27
27
  .action(async (pattern, opts) => {
28
- if (!pattern.endsWith('.xml')) {
28
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
29
29
  pattern += '.xml';
30
30
  }
31
31
  let { javaTests, lang } = opts;
@@ -37,7 +37,10 @@ program
37
37
  lang = lang?.toLowerCase();
38
38
  if (javaTests === true || (lang === 'java' && !javaTests))
39
39
  javaTests = 'src/test/java';
40
- const runReader = new xmlReader_js_1.default({ javaTests, lang });
40
+ const runReader = new xmlReader_js_1.default({
41
+ javaTests,
42
+ lang,
43
+ });
41
44
  const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
42
45
  if (!files.length) {
43
46
  console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found πŸ˜₯`);
package/lib/client.d.ts CHANGED
@@ -44,7 +44,7 @@ export class Client {
44
44
  *
45
45
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
46
46
  */
47
- createRun(params: any): Promise<any>;
47
+ createRun(params?: {}): Promise<any>;
48
48
  /**
49
49
  * Updates test status and its data
50
50
  *
@@ -61,15 +61,5 @@ export class Client {
61
61
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
62
62
  */
63
63
  updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
64
- /**
65
- * Returns the formatted stack including the stack trace, steps, and logs.
66
- * @returns {string}
67
- */
68
- formatLogs({ error, steps, logs }: {
69
- error: any;
70
- steps: any;
71
- logs: any;
72
- }): string;
73
- formatError(error: any, message: any): string;
74
64
  }
75
65
  import { S3Uploader } from './uploader.js';
package/lib/client.js CHANGED
@@ -1,56 +1,21 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.Client = void 0;
40
7
  const debug_1 = __importDefault(require("debug"));
41
- const callsite_record_1 = __importDefault(require("callsite-record"));
42
- const minimatch_1 = require("minimatch");
43
8
  const fs_1 = __importDefault(require("fs"));
44
9
  const picocolors_1 = __importDefault(require("picocolors"));
45
- const crypto_1 = require("crypto");
46
10
  const constants_js_1 = require("./constants.js");
47
11
  const index_js_1 = require("./pipe/index.js");
48
12
  const glob_1 = require("glob");
49
- const path_1 = __importStar(require("path"));
13
+ const path_1 = __importDefault(require("path"));
50
14
  const node_url_1 = require("node:url");
51
15
  const uploader_js_1 = require("./uploader.js");
52
16
  const utils_js_1 = require("./utils/utils.js");
53
17
  const filesize_1 = require("filesize");
18
+ const log_formatter_js_1 = require("./utils/log-formatter.js");
54
19
  const debug = (0, debug_1.default)('@testomatio/reporter:client');
55
20
  // removed __dirname usage, because:
56
21
  // 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
@@ -129,7 +94,7 @@ class Client {
129
94
  *
130
95
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
131
96
  */
132
- async createRun(params) {
97
+ async createRun(params = {}) {
133
98
  if (!this.pipes || !this.pipes.length)
134
99
  this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
135
100
  debug('Creating run...');
@@ -137,7 +102,7 @@ class Client {
137
102
  if (!this.pipes?.filter(p => p.isEnabled).length)
138
103
  return Promise.resolve();
139
104
  this.queue = this.queue
140
- .then(() => Promise.all(this.pipes.map(p => p.createRun())))
105
+ .then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
141
106
  .catch(err => console.log(constants_js_1.APP_PREFIX, err))
142
107
  .then(() => {
143
108
  const runId = this.pipeStore?.runId;
@@ -158,17 +123,6 @@ class Client {
158
123
  * @returns {Promise<PipeResult[]>}
159
124
  */
160
125
  async addTestRun(status, testData) {
161
- if (!this.pipes || !this.pipes.length)
162
- this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
163
- // all pipes disabled, skipping
164
- if (!this.pipes?.filter(p => p.isEnabled).length)
165
- return [];
166
- if (isTestShouldBeExculedFromReport(testData))
167
- return [];
168
- if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
169
- debug('Skipping test from report', testData?.title);
170
- return []; // do not log skipped tests
171
- }
172
126
  if (!testData)
173
127
  testData = {
174
128
  title: 'Unknown test',
@@ -181,9 +135,12 @@ class Client {
181
135
  /**
182
136
  * @type {TestData}
183
137
  */
184
- const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
138
+ const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
139
+ let steps = originalSteps;
140
+ const uploadedFiles = [];
141
+ const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
142
+ const { time = 0, example = null, files = [], filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
185
143
  let { message = '', meta = {} } = testData;
186
- // stringify meta values and limit keys and values length to 255
187
144
  meta = Object.entries(meta)
188
145
  .filter(([, value]) => value !== null && value !== undefined)
189
146
  .reduce((acc, [key, value]) => {
@@ -191,19 +148,38 @@ class Client {
191
148
  acc[key] = value;
192
149
  return acc;
193
150
  }, {});
194
- // Get links from storage using the test context
195
151
  const testContext = suite_title ? `${suite_title} ${title}` : title;
196
152
  let errorFormatted = '';
197
153
  if (error) {
198
- errorFormatted += this.formatError(error) || '';
154
+ errorFormatted += (0, log_formatter_js_1.formatError)(error) || '';
199
155
  message = error?.message;
200
156
  }
201
- // Attach logs
202
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
203
- // add artifacts
157
+ let fullLogs = (0, log_formatter_js_1.formatLogs)({ error: errorFormatted, steps, logs: testData.logs });
158
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
159
+ uploadedFiles.push(this.uploader.uploadFileAsBuffer(Buffer.from((0, log_formatter_js_1.stripColors)(fullLogs), 'utf8'), [
160
+ this.runId,
161
+ rid,
162
+ `logs_${+new Date()}.log`,
163
+ ]));
164
+ fullLogs = '';
165
+ steps = null;
166
+ }
167
+ if (!this.pipes || !this.pipes.length)
168
+ this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
169
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
170
+ if (uploadedFiles.length > 0) {
171
+ await Promise.all(uploadedFiles);
172
+ }
173
+ return [];
174
+ }
175
+ if (isTestShouldBeExcludedFromReport(testData))
176
+ return [];
177
+ if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
178
+ debug('Skipping test from report', testData?.title);
179
+ return [];
180
+ }
204
181
  if (manuallyAttachedArtifacts?.length)
205
182
  files.push(...manuallyAttachedArtifacts);
206
- const uploadedFiles = [];
207
183
  for (let f of files) {
208
184
  if (!f)
209
185
  continue; // f === null
@@ -285,7 +261,7 @@ class Client {
285
261
  const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
286
262
  relativePath: file.path.replace(process.cwd(), ''),
287
263
  link: file.link,
288
- sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
264
+ sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
289
265
  }));
290
266
  uploadedArtifacts.forEach(upload => {
291
267
  debug(`🟒Uploaded artifact`, `${upload.relativePath},`, 'size:', `${upload.sizePretty},`, 'link:', `${upload.link}`);
@@ -295,7 +271,7 @@ class Client {
295
271
  console.log(constants_js_1.APP_PREFIX, `πŸ—„οΈ ${this.uploader.failedUploads.length} artifacts πŸ”΄${picocolors_1.default.bold('failed')} to upload`);
296
272
  const failedUploads = this.uploader.failedUploads.map(file => ({
297
273
  relativePath: file.path.replace(process.cwd(), ''),
298
- sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
274
+ sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
299
275
  }));
300
276
  const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
301
277
  failedUploads.forEach(upload => {
@@ -323,93 +299,14 @@ class Client {
323
299
  .catch(err => console.log(constants_js_1.APP_PREFIX, err));
324
300
  return this.queue;
325
301
  }
326
- /**
327
- * Returns the formatted stack including the stack trace, steps, and logs.
328
- * @returns {string}
329
- */
330
- formatLogs({ error, steps, logs }) {
331
- error = error?.trim();
332
- logs = logs?.trim().split('\n').map(l => (0, utils_js_1.truncate)(l)).join('\n');
333
- if (Array.isArray(steps)) {
334
- steps = steps
335
- .map(step => (0, utils_js_1.formatStep)(step))
336
- .flat()
337
- .join('\n');
338
- }
339
- let testLogs = '';
340
- if (steps)
341
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.blue('################[ Steps ]################'))}\n${steps}\n\n`;
342
- if (logs)
343
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.gray('################[ Logs ]################'))}\n${logs}\n\n`;
344
- if (error)
345
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'))}\n${error}`;
346
- return testLogs;
347
- }
348
- formatError(error, message) {
349
- if (!message)
350
- message = error.message;
351
- if (error.inspect)
352
- message = error.inspect() || '';
353
- let stack = '';
354
- if (error.name)
355
- stack += `${picocolors_1.default.red(error.name)}`;
356
- if (error.operator)
357
- stack += ` (${picocolors_1.default.red(error.operator)})`;
358
- // add new line if something was added to stack
359
- if (stack)
360
- stack += ': ';
361
- stack += `${message}\n`;
362
- if (error.diff) {
363
- // diff for vitest
364
- stack += error.diff;
365
- stack += '\n\n';
366
- }
367
- else if (error.actual && error.expected && error.actual !== error.expected) {
368
- // diffs for mocha, cypress, codeceptjs style
369
- stack += `\n\n${picocolors_1.default.bold(picocolors_1.default.green('+ expected'))} ${picocolors_1.default.bold(picocolors_1.default.red('- actual'))}`;
370
- stack += `\n${picocolors_1.default.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
371
- stack += `\n${picocolors_1.default.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
372
- stack += '\n\n';
373
- }
374
- const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
375
- try {
376
- let hasFrame = false;
377
- const record = (0, callsite_record_1.default)({
378
- forError: error,
379
- isCallsiteFrame: frame => {
380
- if (customFilter && (0, minimatch_1.minimatch)(frame.fileName, customFilter))
381
- return false;
382
- if (hasFrame)
383
- return false;
384
- if (isNotInternalFrame(frame))
385
- hasFrame = true;
386
- return hasFrame;
387
- },
388
- });
389
- // @ts-ignore
390
- if (record && !record.filename.startsWith('http')) {
391
- stack += record.renderSync({ stackFilter: isNotInternalFrame });
392
- }
393
- return stack;
394
- }
395
- catch (e) {
396
- console.log(e);
397
- }
398
- }
399
302
  }
400
303
  exports.Client = Client;
401
- function isNotInternalFrame(frame) {
402
- return (frame.getFileName() &&
403
- frame.getFileName().includes(path_1.sep) &&
404
- !frame.getFileName().includes('node_modules') &&
405
- !frame.getFileName().includes('internal'));
406
- }
407
304
  /**
408
305
  *
409
306
  * @param {TestData} testData
410
307
  * @returns boolean
411
308
  */
412
- function isTestShouldBeExculedFromReport(testData) {
309
+ function isTestShouldBeExcludedFromReport(testData) {
413
310
  // const fileName = path.basename(test.location?.file || '');
414
311
  const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
415
312
  if (!globExcludeFilesPattern)
@@ -418,11 +315,11 @@ function isTestShouldBeExculedFromReport(testData) {
418
315
  debug('No "file" property found for test ', testData.title);
419
316
  return false;
420
317
  }
421
- const excludeParretnsList = globExcludeFilesPattern.split(';');
318
+ const excludePatternsList = globExcludeFilesPattern.split(';');
422
319
  // as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
423
320
  if (!listOfTestFilesToExcludeFromReport) {
424
321
  // list of files with relative paths
425
- listOfTestFilesToExcludeFromReport = glob_1.glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
322
+ listOfTestFilesToExcludeFromReport = glob_1.glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
426
323
  debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
427
324
  }
428
325
  const testFileRelativePath = path_1.default.relative(process.cwd(), testData.file);
@@ -1,5 +1,4 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
- getFilePath(t: any): string;
4
3
  }
5
4
  import Adapter from './adapter.js';
@@ -7,24 +7,60 @@ const path_1 = __importDefault(require("path"));
7
7
  const adapter_js_1 = __importDefault(require("./adapter.js"));
8
8
  class CSharpAdapter extends adapter_js_1.default {
9
9
  formatTest(t) {
10
- const title = t.title.replace(/\(.*?\)/, '').trim();
11
- const example = t.title.match(/\((.*?)\)/);
12
- if (example)
13
- t.example = { ...example[1].split(',') };
10
+ // Extract example from title if not already present
11
+ if (!t.example) {
12
+ const exampleMatch = t.title.match(/\((.*?)\)/);
13
+ if (exampleMatch) {
14
+ // Extract parameters as object with numeric keys for API
15
+ const params = exampleMatch[1]
16
+ .split(',')
17
+ .map(param => param.trim())
18
+ .filter(param => param !== '');
19
+ t.example = {};
20
+ params.forEach((param, index) => {
21
+ t.example[index] = param;
22
+ });
23
+ }
24
+ }
25
+ // Remove parameters from title to avoid duplicates in Test Suite
26
+ // The example field will be used for grouping on import
27
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
14
28
  const suite = t.suite_title.split('.');
15
29
  t.suite_title = suite.pop();
16
30
  t.file = namespaceToFileName(t.file);
17
- t.title = title.trim();
18
31
  return t;
19
32
  }
20
33
  getFilePath(t) {
21
- const fileName = namespaceToFileName(t.file);
34
+ if (!t.file)
35
+ return null;
36
+ // Normalize path separators for cross-platform compatibility
37
+ let filePath = t.file.replace(/\\/g, '/');
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_1.default.isAbsolute(filePath)) {
42
+ // Try to find project-relative path
43
+ const cwd = process.cwd().replace(/\\/g, '/');
44
+ if (filePath.startsWith(cwd)) {
45
+ filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
46
+ }
47
+ }
48
+ return filePath;
49
+ }
50
+ // Convert namespace path to file path
51
+ const fileName = namespaceToFileName(filePath);
22
52
  return fileName;
23
53
  }
24
54
  }
25
55
  module.exports = CSharpAdapter;
26
56
  function namespaceToFileName(fileName) {
57
+ if (!fileName)
58
+ return '';
59
+ // If already a .cs file path, clean it up
60
+ if (fileName.endsWith('.cs')) {
61
+ return fileName.replace(/\\/g, '/');
62
+ }
27
63
  const fileParts = fileName.split('.');
28
64
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
29
- return `${fileParts.join(path_1.default.sep)}.cs`;
65
+ return `${fileParts.join('/')}.cs`;
30
66
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
3
+ * and parameterized tests
4
+ */
5
+ export class NUnitXmlParser {
6
+ constructor(options?: {});
7
+ options: {};
8
+ tests: any[];
9
+ stats: {
10
+ total: number;
11
+ passed: number;
12
+ failed: number;
13
+ skipped: number;
14
+ inconclusive: number;
15
+ };
16
+ /**
17
+ * Parse NUnit XML test-run structure
18
+ * @param {Object} testRun - Parsed XML test-run object
19
+ * @returns {Object} - Parsed test results
20
+ */
21
+ parseTestRun(testRun: any): any;
22
+ /**
23
+ * Recursively parse test-suite elements based on their type
24
+ * @param {Object|Array} testSuite - Test suite object or array
25
+ * @param {Array} parentPath - Current path in the hierarchy
26
+ */
27
+ parseTestSuite(testSuite: any | any[], parentPath?: any[]): void;
28
+ /**
29
+ * Process child elements of a test suite
30
+ * @param {Object} testSuite - Test suite object
31
+ * @param {Array} currentPath - Current path in hierarchy
32
+ */
33
+ processChildren(testSuite: any, currentPath: any[]): void;
34
+ /**
35
+ * Parse test-case elements (actual tests)
36
+ * @param {Object|Array} testCases - Test case object or array
37
+ * @param {Array} suitePath - Path to the test suite
38
+ * @param {Object} parentSuite - Parent test suite for context
39
+ */
40
+ parseTestCases(testCases: any | any[], suitePath: any[], parentSuite: any): void;
41
+ /**
42
+ * Parse individual test case
43
+ * @param {Object} testCase - Test case object
44
+ * @param {Array} suitePath - Path to the test suite
45
+ * @param {Object} parentSuite - Parent test suite for context
46
+ * @returns {Object|null} - Parsed test object
47
+ */
48
+ parseTestCase(testCase: any, suitePath: any[], parentSuite: any): any | null;
49
+ /**
50
+ * Extract method name and parameters from test name
51
+ * @param {string} testName - Full test name
52
+ * @returns {Object} - Extracted information
53
+ */
54
+ extractParameters(testName: string): any;
55
+ /**
56
+ * Parse parameter string into array of parameters
57
+ * @param {string} paramString - Parameter string
58
+ * @returns {Array} - Array of parameters
59
+ */
60
+ parseParameterString(paramString: string): any[];
61
+ /**
62
+ * Extract method name from test name (fallback)
63
+ * @param {string} testName - Test name
64
+ * @returns {string} - Method name
65
+ */
66
+ extractMethodName(testName: string): string;
67
+ /**
68
+ * Build file path from suite path and class name
69
+ * @param {Array} suitePath - Suite path array
70
+ * @param {string} className - Class name
71
+ * @param {Object} parentSuite - Parent suite for context
72
+ * @returns {string} - File path
73
+ */
74
+ buildFilePath(suitePath: any[], className: string, parentSuite: any): string;
75
+ /**
76
+ * Group parameterized tests by base method name
77
+ * @param {Array} tests - Array of parsed tests
78
+ * @returns {Object} - Grouped tests
79
+ */
80
+ groupParameterizedTests(tests: any[]): any;
81
+ }
82
+ export default NUnitXmlParser;