@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/lib/reporter.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export { Client };
2
+ export const STATUS: {
3
+ PASSED: string;
4
+ FAILED: string;
5
+ SKIPPED: string;
6
+ FINISHED: string;
7
+ };
1
8
  export const artifact: (data: string | {
2
9
  path: string;
3
10
  type: string;
@@ -80,7 +87,7 @@ export const label: (key: string, value?: string | null) => void;
80
87
  export const linkTest: (...testIds: string[]) => void;
81
88
  export const linkJira: (...jiraIds: string[]) => void;
82
89
  declare namespace _default {
83
- let testomatioLogger: {
90
+ export let testomatioLogger: {
84
91
  "__#13@#originalUserLogger": {
85
92
  assert(condition?: boolean, ...data: any[]): void;
86
93
  assert(value: any, message?: string, ...optionalParams: any[]): void;
@@ -148,13 +155,13 @@ declare namespace _default {
148
155
  }): void;
149
156
  prettyObjects: boolean;
150
157
  };
151
- let artifact: (data: string | {
158
+ export let artifact: (data: string | {
152
159
  path: string;
153
160
  type: string;
154
161
  name: string;
155
162
  }, context?: any) => void;
156
- let log: (...args: any[]) => void;
157
- let logger: {
163
+ export let log: (...args: any[]) => void;
164
+ export let logger: {
158
165
  "__#13@#originalUserLogger": {
159
166
  assert(condition?: boolean, ...data: any[]): void;
160
167
  assert(value: any, message?: string, ...optionalParams: any[]): void;
@@ -222,13 +229,15 @@ declare namespace _default {
222
229
  }): void;
223
230
  prettyObjects: boolean;
224
231
  };
225
- let meta: (keyValue: {
232
+ export let meta: (keyValue: {
226
233
  [key: string]: string;
227
234
  } | string, value?: string | null) => void;
228
- let step: (message: string) => void;
229
- let label: (key: string, value?: string | null) => void;
230
- let linkTest: (...testIds: string[]) => void;
231
- let linkJira: (...jiraIds: string[]) => void;
235
+ export let step: (message: string) => void;
236
+ export let label: (key: string, value?: string | null) => void;
237
+ export let linkTest: (...testIds: string[]) => void;
238
+ export let linkJira: (...jiraIds: string[]) => void;
239
+ export { Client as TestomatioClient };
240
+ export { STATUS };
232
241
  }
233
242
  export default _default;
234
243
  export type ArtifactFunction = typeof import("./reporter-functions.js").default.artifact;
@@ -237,3 +246,4 @@ export type LoggerService = typeof import("./services/index.js").services.logger
237
246
  export type MetaFunction = typeof import("./reporter-functions.js").default.keyValue;
238
247
  export type StepFunction = typeof import("./reporter-functions.js").default.step;
239
248
  export type LabelFunction = typeof import("./reporter-functions.js").default.label;
249
+ import Client from './client.js';
package/lib/reporter.js CHANGED
@@ -1,13 +1,48 @@
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.linkJira = exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
7
- // import TestomatClient from './client.js';
8
- // import * as TRConstants from './constants.js';
39
+ exports.linkJira = exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = exports.STATUS = exports.Client = void 0;
40
+ const client_js_1 = __importDefault(require("./client.js"));
41
+ exports.Client = client_js_1.default;
42
+ const TestomatioConstants = __importStar(require("./constants.js"));
9
43
  const index_js_1 = require("./services/index.js");
10
44
  const reporter_functions_js_1 = __importDefault(require("./reporter-functions.js"));
45
+ exports.STATUS = TestomatioConstants.STATUS;
11
46
  exports.artifact = reporter_functions_js_1.default.artifact;
12
47
  exports.log = reporter_functions_js_1.default.log;
13
48
  exports.logger = index_js_1.services.logger;
@@ -37,6 +72,6 @@ module.exports = {
37
72
  label: reporter_functions_js_1.default.label,
38
73
  linkTest: reporter_functions_js_1.default.linkTest,
39
74
  linkJira: reporter_functions_js_1.default.linkJira,
40
- // TestomatClient,
41
- // TRConstants,
75
+ TestomatioClient: client_js_1.default,
76
+ STATUS: exports.STATUS,
42
77
  };
package/lib/uploader.js CHANGED
@@ -170,6 +170,10 @@ class S3Uploader {
170
170
  if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
171
171
  filePath = path_1.default.join(process.cwd(), filePath);
172
172
  }
173
+ // Normalize path separators for cross-platform compatibility
174
+ if (typeof filePath === 'string') {
175
+ filePath = filePath.replace(/\\/g, '/');
176
+ }
173
177
  const data = { rid, file: filePath, uploaded };
174
178
  const jsonLine = `${JSON.stringify(data)}\n`;
175
179
  fs_1.default.appendFileSync(tempFilePath, jsonLine);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Returns the formatted stack including the stack trace, steps, and logs.
3
+ * @param {Object} params - Parameters for formatting logs
4
+ * @param {string} params.error - Error message
5
+ * @param {Array|any} params.steps - Test steps (array or other types)
6
+ * @param {string} params.logs - Test logs
7
+ * @returns {string}
8
+ */
9
+ export function formatLogs({ error, steps, logs }: {
10
+ error: string;
11
+ steps: any[] | any;
12
+ logs: string;
13
+ }): string;
14
+ /**
15
+ * Formats an error with stack trace and diff information
16
+ * @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
17
+ * The error object to format
18
+ * @param {string} [message] - Optional error message override
19
+ * @returns {string}
20
+ */
21
+ export function formatError(error: Error & {
22
+ inspect?: () => string;
23
+ operator?: string;
24
+ diff?: string;
25
+ actual?: any;
26
+ expected?: any;
27
+ }, message?: string): string;
28
+ export function stripColors(str: string): string;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.stripColors = void 0;
7
+ exports.formatLogs = formatLogs;
8
+ exports.formatError = formatError;
9
+ const callsite_record_1 = __importDefault(require("callsite-record"));
10
+ const minimatch_1 = require("minimatch");
11
+ const picocolors_1 = __importDefault(require("picocolors"));
12
+ const util_1 = require("util");
13
+ const path_1 = require("path");
14
+ const utils_js_1 = require("./utils.js");
15
+ const stripColors = util_1.stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
16
+ exports.stripColors = stripColors;
17
+ /**
18
+ * Returns the formatted stack including the stack trace, steps, and logs.
19
+ * @param {Object} params - Parameters for formatting logs
20
+ * @param {string} params.error - Error message
21
+ * @param {Array|any} params.steps - Test steps (array or other types)
22
+ * @param {string} params.logs - Test logs
23
+ * @returns {string}
24
+ */
25
+ function formatLogs({ error, steps, logs }) {
26
+ error = error?.trim();
27
+ logs = logs
28
+ ?.trim()
29
+ .split('\n')
30
+ .map(l => (0, utils_js_1.truncate)(l))
31
+ .join('\n');
32
+ if (Array.isArray(steps)) {
33
+ steps = steps
34
+ .map(step => (0, utils_js_1.formatStep)(step))
35
+ .flat()
36
+ .join('\n');
37
+ }
38
+ else {
39
+ steps = null;
40
+ }
41
+ let testLogs = '';
42
+ if (steps)
43
+ testLogs += `${picocolors_1.default.bold(picocolors_1.default.blue('################[ Steps ]################'))}\n${steps}\n\n`;
44
+ if (logs)
45
+ testLogs += `${picocolors_1.default.bold(picocolors_1.default.gray('################[ Logs ]################'))}\n${logs}\n\n`;
46
+ if (error)
47
+ testLogs += `${picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'))}\n${error}`;
48
+ return testLogs;
49
+ }
50
+ /**
51
+ * Formats an error with stack trace and diff information
52
+ * @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
53
+ * The error object to format
54
+ * @param {string} [message] - Optional error message override
55
+ * @returns {string}
56
+ */
57
+ function formatError(error, message) {
58
+ if (!message)
59
+ message = error.message;
60
+ // @ts-ignore - inspect is a custom property added by some testing frameworks
61
+ if (error.inspect)
62
+ message = error.inspect() || '';
63
+ let stack = '';
64
+ if (error.name)
65
+ stack += `${picocolors_1.default.red(error.name)}`;
66
+ // @ts-ignore - operator is a custom property added by assertion libraries
67
+ if (error.operator)
68
+ stack += ` (${picocolors_1.default.red(error.operator)})`;
69
+ // add new line if something was added to stack
70
+ if (stack)
71
+ stack += ': ';
72
+ stack += `${message}\n`;
73
+ // @ts-ignore - diff is a custom property added by vitest
74
+ if (error.diff) {
75
+ // diff for vitest
76
+ stack += error.diff;
77
+ stack += '\n\n';
78
+ }
79
+ else if (error.actual && error.expected && error.actual !== error.expected) {
80
+ // diffs for mocha, cypress, codeceptjs style
81
+ stack += `\n\n${picocolors_1.default.bold(picocolors_1.default.green('+ expected'))} ${picocolors_1.default.bold(picocolors_1.default.red('- actual'))}`;
82
+ stack += `\n${picocolors_1.default.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
83
+ stack += `\n${picocolors_1.default.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
84
+ stack += '\n\n';
85
+ }
86
+ const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
87
+ try {
88
+ let hasFrame = false;
89
+ const record = (0, callsite_record_1.default)({
90
+ forError: error,
91
+ isCallsiteFrame: frame => {
92
+ if (customFilter && (0, minimatch_1.minimatch)(frame.fileName, customFilter))
93
+ return false;
94
+ if (hasFrame)
95
+ return false;
96
+ if (isNotInternalFrame(frame))
97
+ hasFrame = true;
98
+ return hasFrame;
99
+ },
100
+ });
101
+ // @ts-ignore
102
+ if (record && !record.filename.startsWith('http')) {
103
+ stack += record.renderSync({ stackFilter: isNotInternalFrame });
104
+ }
105
+ return stack;
106
+ }
107
+ catch (e) {
108
+ console.log(e);
109
+ }
110
+ }
111
+ /**
112
+ * Checks if a stack frame is not an internal frame (node_modules or internal)
113
+ * @param {Object} frame - Stack frame object
114
+ * @returns {boolean}
115
+ */
116
+ function isNotInternalFrame(frame) {
117
+ return (frame.getFileName() &&
118
+ frame.getFileName().includes(path_1.sep) &&
119
+ !frame.getFileName().includes('node_modules') &&
120
+ !frame.getFileName().includes('internal'));
121
+ }
122
+
123
+ module.exports.formatLogs = formatLogs;
124
+
125
+ module.exports.formatError = formatError;
126
+
127
+ module.exports.stripColors = stripColors;
@@ -116,19 +116,26 @@ const isValidUrl = s => {
116
116
  }
117
117
  };
118
118
  exports.isValidUrl = isValidUrl;
119
- const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
119
+ const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
120
120
  const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
121
- const files = Array.from(stack.matchAll(fileMatchRegex))
122
- .map(f => f[1].trim())
121
+ let files = Array.from(stack.matchAll(fileMatchRegex))
122
+ .map(match => {
123
+ // match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
124
+ const slashes = match[1] || '';
125
+ const path = match[2];
126
+ const extension = match[3];
127
+ return `${slashes}${path}.${extension}`;
128
+ })
129
+ .map(f => f.trim())
123
130
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
124
131
  .map(f => {
125
- // Convert Windows paths to Linux paths for testing purposes
126
- if (f.match(/^[A-Za-z]:[\\\/]/)) {
127
- // Convert Windows path to Linux equivalent for test scenarios
128
- return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
129
- }
130
- return f;
132
+ // Normalize path separators for cross-platform compatibility
133
+ return f.replace(/\\/g, '/');
131
134
  });
135
+ // If we're not checking file existence, remove Windows drive letters for consistency
136
+ if (!checkExists) {
137
+ files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
138
+ }
132
139
  debug('Found files in stack trace: ', files);
133
140
  return files.filter(f => {
134
141
  if (!checkExists)
@@ -144,19 +151,88 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
144
151
  const stackLines = stack
145
152
  .split('\n')
146
153
  .filter(l => l.includes(':'))
147
- // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
148
- // .map(l => l.split(':')[0])
149
154
  .map(l => l.trim())
150
- .map(l => l.split(' ').find(p => p.includes(':')) || '')
151
- .filter(l => (0, is_valid_path_1.default)(l?.split(':')[0]))
155
+ .map(l => {
156
+ // Remove 'at ' prefix if present
157
+ if (l.startsWith('at ')) {
158
+ return l.substring(3).trim();
159
+ }
160
+ // Find the part that looks like a file path with line number
161
+ const parts = l.split(' ');
162
+ for (const part of parts) {
163
+ // Check if this part has a colon
164
+ if (part.includes(':')) {
165
+ // For Windows paths, we need to handle drive letters (C:, D:, etc.)
166
+ // Split by colon but keep drive letter with the path
167
+ const colonParts = part.split(':');
168
+ let filePath;
169
+ // Check if first part is a Windows drive letter (single letter)
170
+ if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
171
+ // Windows path like D:\path\file.php:24
172
+ // Reconstruct as D:\path\file.php
173
+ filePath = colonParts[0] + ':' + colonParts[1];
174
+ }
175
+ else {
176
+ // Unix path like /path/file.php:24
177
+ filePath = colonParts[0];
178
+ }
179
+ // Only consider it valid if the file exists
180
+ if (fs_1.default.existsSync(filePath)) {
181
+ return part;
182
+ }
183
+ }
184
+ }
185
+ // If no valid file path found in parts, return the whole line
186
+ // It will be filtered out later if it's not a valid file path
187
+ return parts.find(p => p.includes(':')) || l;
188
+ })
189
+ .filter(l => {
190
+ // Extract file path from line (accounting for Windows drive letters)
191
+ if (!l)
192
+ return false;
193
+ const colonParts = l.split(':');
194
+ let filePath;
195
+ if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
196
+ // Windows path
197
+ filePath = colonParts[0] + ':' + colonParts[1];
198
+ }
199
+ else {
200
+ // Unix path
201
+ filePath = colonParts[0];
202
+ }
203
+ return filePath && fs_1.default.existsSync(filePath);
204
+ })
152
205
  // // filter out 3rd party libs
153
206
  .filter(l => !l?.includes(`vendor${path_1.sep}`))
154
207
  .filter(l => !l?.includes(`node_modules${path_1.sep}`))
155
- .filter(l => fs_1.default.existsSync(l.split(':')[0]))
156
- .filter(l => fs_1.default.lstatSync(l.split(':')[0]).isFile());
208
+ .filter(l => {
209
+ // Extract file path for final check (accounting for Windows drive letters)
210
+ const colonParts = l.split(':');
211
+ let filePath;
212
+ if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
213
+ filePath = colonParts[0] + ':' + colonParts[1];
214
+ }
215
+ else {
216
+ filePath = colonParts[0];
217
+ }
218
+ return fs_1.default.lstatSync(filePath).isFile();
219
+ });
157
220
  if (!stackLines.length)
158
221
  return '';
159
- const [file, line] = stackLines[0].split(':');
222
+ // Extract file and line number (accounting for Windows drive letters)
223
+ const firstLine = stackLines[0];
224
+ const colonParts = firstLine.split(':');
225
+ let file, line;
226
+ if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
227
+ // Windows path like D:\path\file.php:24
228
+ file = colonParts[0] + ':' + colonParts[1];
229
+ line = colonParts[2];
230
+ }
231
+ else {
232
+ // Unix path like /path/file.php:24
233
+ file = colonParts[0];
234
+ line = colonParts[1];
235
+ }
160
236
  const prepend = 3;
161
237
  const source = fetchSourceCode(fs_1.default.readFileSync(file).toString(), { line, prepend, limit: 7 });
162
238
  if (!source)
@@ -174,6 +250,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
174
250
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
175
251
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
176
252
  const fetchIdFromCode = (code, opts = {}) => {
253
+ if (!code)
254
+ return null;
177
255
  const comments = code
178
256
  .split('\n')
179
257
  .map(l => l.trim())
@@ -216,10 +294,58 @@ const fetchSourceCode = (contents, opts = {}) => {
216
294
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
217
295
  }
218
296
  else if (opts.lang === 'csharp') {
219
- if (lineIndex === -1)
220
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
221
- if (lineIndex === -1)
222
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
297
+ // Find the method declaration line
298
+ let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
299
+ if (methodLineIndex === -1) {
300
+ methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
301
+ }
302
+ if (methodLineIndex === -1) {
303
+ methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
304
+ }
305
+ // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
306
+ if (methodLineIndex !== -1) {
307
+ lineIndex = methodLineIndex;
308
+ // Scan upwards to find the start of attributes and comments
309
+ for (let i = methodLineIndex - 1; i >= 0; i--) {
310
+ const trimmedLine = lines[i].trim();
311
+ // Include [TestCase], [Test], and other attributes
312
+ if (trimmedLine.startsWith('[')) {
313
+ lineIndex = i;
314
+ continue;
315
+ }
316
+ // Include XML documentation comments
317
+ if (trimmedLine.startsWith('///')) {
318
+ lineIndex = i;
319
+ continue;
320
+ }
321
+ // Stop at empty lines (with some tolerance)
322
+ if (trimmedLine === '') {
323
+ // Check if next non-empty line is an attribute or comment
324
+ let hasMoreAttributes = false;
325
+ for (let j = i - 1; j >= 0; j--) {
326
+ const nextTrimmed = lines[j].trim();
327
+ if (nextTrimmed === '')
328
+ continue;
329
+ if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
330
+ hasMoreAttributes = true;
331
+ lineIndex = j;
332
+ }
333
+ break;
334
+ }
335
+ if (!hasMoreAttributes)
336
+ break;
337
+ continue;
338
+ }
339
+ // Stop at other method declarations or class-level elements
340
+ if (trimmedLine.includes('public ') ||
341
+ trimmedLine.includes('private ') ||
342
+ trimmedLine.includes('protected ') ||
343
+ trimmedLine.includes('internal ')) {
344
+ if (!trimmedLine.startsWith('['))
345
+ break;
346
+ }
347
+ }
348
+ }
223
349
  }
224
350
  else {
225
351
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -228,11 +354,28 @@ const fetchSourceCode = (contents, opts = {}) => {
228
354
  if (opts.prepend) {
229
355
  lineIndex -= opts.prepend;
230
356
  }
231
- if (lineIndex) {
357
+ if (lineIndex !== -1 && lineIndex !== undefined) {
232
358
  const result = [];
359
+ let braceDepth = 0; // Track brace depth for C# methods
360
+ let methodStartFound = false; // Flag to indicate we've found the method opening brace
233
361
  for (let i = lineIndex; i < lineIndex + limit; i++) {
234
362
  if (lines[i] === undefined)
235
363
  continue;
364
+ // Track brace depth for C# to stop after method closes
365
+ if (opts.lang === 'csharp') {
366
+ const line = lines[i];
367
+ // Count opening and closing braces
368
+ const openBraces = (line.match(/\{/g) || []).length;
369
+ const closeBraces = (line.match(/\}/g) || []).length;
370
+ if (openBraces > 0)
371
+ methodStartFound = true;
372
+ braceDepth += openBraces - closeBraces;
373
+ // If we've started the method and depth returns to 0, method is complete
374
+ if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
375
+ // Don't include the closing brace - just break
376
+ break;
377
+ }
378
+ }
236
379
  if (i > lineIndex + 2 && !opts.prepend) {
237
380
  // annotation
238
381
  if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
@@ -271,6 +414,24 @@ const fetchSourceCode = (contents, opts = {}) => {
271
414
  break;
272
415
  if (opts.lang === 'java' && lines[i].includes(' class '))
273
416
  break;
417
+ // For C#, additional checks if brace tracking didn't stop us
418
+ if (opts.lang === 'csharp') {
419
+ const trimmed = lines[i].trim();
420
+ // Stop at attribute that marks beginning of next test (but not if we're still in the current method)
421
+ if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0)
422
+ break;
423
+ // Stop at XML documentation comments that belong to next method
424
+ if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0)
425
+ break;
426
+ // Stop at another method declaration (but not if we're still in the current method)
427
+ if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
428
+ methodStartFound &&
429
+ braceDepth === 0)
430
+ break;
431
+ // Stop at class declaration
432
+ if (trimmed.includes(' class ') && trimmed.includes('public'))
433
+ break;
434
+ }
274
435
  }
275
436
  result.push(lines[i]);
276
437
  }
@@ -471,10 +632,14 @@ function transformEnvVarToBoolean(value) {
471
632
  return Boolean(value);
472
633
  }
473
634
  function truncate(s, size = 255) {
474
- if (s.toString().trim().length < size) {
475
- return s.toString();
635
+ if (s === undefined || s === null) {
636
+ return '';
637
+ }
638
+ const str = s.toString();
639
+ if (str.trim().length < size) {
640
+ return str;
476
641
  }
477
- return `${s.toString().substring(0, size)}...`;
642
+ return `${str.substring(0, size)}...`;
478
643
  }
479
644
 
480
645
  module.exports.getPackageVersion = getPackageVersion;
@@ -19,25 +19,11 @@ declare class XmlReader {
19
19
  tests: any[];
20
20
  stats: {};
21
21
  uploader: S3Uploader;
22
+ enhancedNunit: boolean;
23
+ groupParameterized: boolean;
22
24
  version: any;
23
25
  connectAdapter(): import("./junit-adapter/adapter.js").default;
24
- parse(fileName: any): {
25
- status: string;
26
- create_tests: boolean;
27
- tests_count: number;
28
- passed_count: number;
29
- skipped_count: number;
30
- failed_count: number;
31
- tests: any;
32
- } | {
33
- status: any;
34
- create_tests: boolean;
35
- tests_count: number;
36
- passed_count: number;
37
- failed_count: number;
38
- skipped_count: number;
39
- tests: any[];
40
- };
26
+ parse(fileName: any): any;
41
27
  processJUnit(jsonSuite: any): {
42
28
  create_tests: boolean;
43
29
  duration: number;
@@ -49,15 +35,14 @@ declare class XmlReader {
49
35
  tests: any[];
50
36
  tests_count: number;
51
37
  };
52
- processNUnit(jsonSuite: any): {
53
- status: any;
54
- create_tests: boolean;
55
- tests_count: number;
56
- passed_count: number;
57
- failed_count: number;
58
- skipped_count: number;
59
- tests: any[];
60
- };
38
+ processNUnit(jsonSuite: any): any;
39
+ /**
40
+ * Check if the XML is actually NUnit format (has test-suite hierarchy)
41
+ * @param {Object} jsonSuite - Parsed XML suite object
42
+ * @returns {boolean} - True if this is NUnit XML format
43
+ */
44
+ isNUnitXml(jsonSuite: any): boolean;
45
+ processNUnitEnhanced(jsonSuite: any): any;
61
46
  processTRX(jsonSuite: any): {
62
47
  status: string;
63
48
  create_tests: boolean;
@@ -67,6 +52,27 @@ declare class XmlReader {
67
52
  failed_count: number;
68
53
  tests: any;
69
54
  };
55
+ _parseTRXTestDefinition(td: any): {
56
+ title: any;
57
+ example: any;
58
+ file: string;
59
+ description: any;
60
+ suite_title: any;
61
+ id: any;
62
+ };
63
+ _parseTRXTestResult(td: any, tests: any): {
64
+ suite_title: any;
65
+ title: any;
66
+ file: any;
67
+ description: any;
68
+ code: any;
69
+ run_time: number;
70
+ stack: any;
71
+ files: any;
72
+ create: boolean;
73
+ overwrite: boolean;
74
+ };
75
+ _mapTRXStatus(outcome: any): string;
70
76
  processXUnit(assemblies: any): {
71
77
  status: string;
72
78
  create_tests: boolean;