@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-beta.100
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.
- package/lib/adapter/codecept.js +22 -2
- package/lib/bin/cli.js +13 -3
- package/lib/bin/reportXml.js +1 -4
- package/lib/bin/startTest.js +3 -3
- package/lib/bin/uploadArtifacts.js +0 -0
- package/lib/client.d.ts +1 -1
- package/lib/client.js +32 -23
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -36
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.d.ts +2 -1
- package/lib/pipe/testomatio.js +17 -21
- package/lib/reporter.d.ts +19 -9
- package/lib/reporter.js +40 -5
- package/lib/template/testomatio.hbs +1366 -1026
- package/lib/uploader.js +0 -4
- package/lib/utils/utils.d.ts +1 -0
- package/lib/utils/utils.js +23 -35
- package/lib/xmlReader.d.ts +26 -11
- package/lib/xmlReader.js +1 -50
- package/package.json +8 -4
- package/src/adapter/codecept.js +27 -3
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +1 -1
- package/src/bin/startTest.js +5 -5
- package/src/client.js +55 -27
- package/src/junit-adapter/csharp.js +11 -6
- package/src/junit-adapter/nunit-parser.js +83 -12
- package/src/pipe/debug.js +3 -2
- package/src/pipe/testomatio.js +81 -77
- package/src/reporter.js +7 -4
- package/src/template/testomatio.hbs +1366 -1026
- package/src/utils/utils.js +205 -32
- package/src/xmlReader.js +70 -45
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
- package/lib/junit-adapter/nunit-parser.d.ts +0 -82
- package/lib/junit-adapter/nunit-parser.js +0 -357
package/src/utils/utils.js
CHANGED
|
@@ -76,17 +76,29 @@ const isValidUrl = s => {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const fileMatchRegex = /file:(
|
|
79
|
+
const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
|
|
80
80
|
|
|
81
81
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
82
|
-
|
|
83
|
-
.map(
|
|
82
|
+
let files = Array.from(stack.matchAll(fileMatchRegex))
|
|
83
|
+
.map(match => {
|
|
84
|
+
// match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
|
|
85
|
+
const slashes = match[1] || '';
|
|
86
|
+
const path = match[2];
|
|
87
|
+
const extension = match[3];
|
|
88
|
+
return `${slashes}${path}.${extension}`;
|
|
89
|
+
})
|
|
90
|
+
.map(f => f.trim())
|
|
84
91
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
85
92
|
.map(f => {
|
|
86
93
|
// Normalize path separators for cross-platform compatibility
|
|
87
94
|
return f.replace(/\\/g, '/');
|
|
88
95
|
});
|
|
89
96
|
|
|
97
|
+
// If we're not checking file existence, remove Windows drive letters for consistency
|
|
98
|
+
if (!checkExists) {
|
|
99
|
+
files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
debug('Found files in stack trace: ', files);
|
|
91
103
|
|
|
92
104
|
return files.filter(f => {
|
|
@@ -101,21 +113,92 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
101
113
|
const stackLines = stack
|
|
102
114
|
.split('\n')
|
|
103
115
|
.filter(l => l.includes(':'))
|
|
104
|
-
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
105
|
-
// .map(l => l.split(':')[0])
|
|
106
116
|
.map(l => l.trim())
|
|
107
|
-
.map(l =>
|
|
108
|
-
|
|
117
|
+
.map(l => {
|
|
118
|
+
// Remove 'at ' prefix if present
|
|
119
|
+
if (l.startsWith('at ')) {
|
|
120
|
+
return l.substring(3).trim();
|
|
121
|
+
}
|
|
122
|
+
// Find the part that looks like a file path with line number
|
|
123
|
+
const parts = l.split(' ');
|
|
124
|
+
for (const part of parts) {
|
|
125
|
+
// Check if this part has a colon
|
|
126
|
+
if (part.includes(':')) {
|
|
127
|
+
// For Windows paths, we need to handle drive letters (C:, D:, etc.)
|
|
128
|
+
// Split by colon but keep drive letter with the path
|
|
129
|
+
const colonParts = part.split(':');
|
|
130
|
+
let filePath;
|
|
131
|
+
|
|
132
|
+
// Check if first part is a Windows drive letter (single letter)
|
|
133
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
134
|
+
// Windows path like D:\path\file.php:24
|
|
135
|
+
// Reconstruct as D:\path\file.php
|
|
136
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
137
|
+
} else {
|
|
138
|
+
// Unix path like /path/file.php:24
|
|
139
|
+
filePath = colonParts[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Only consider it valid if the file exists
|
|
143
|
+
if (fs.existsSync(filePath)) {
|
|
144
|
+
return part;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// If no valid file path found in parts, return the whole line
|
|
149
|
+
// It will be filtered out later if it's not a valid file path
|
|
150
|
+
return parts.find(p => p.includes(':')) || l;
|
|
151
|
+
})
|
|
152
|
+
.filter(l => {
|
|
153
|
+
// Extract file path from line (accounting for Windows drive letters)
|
|
154
|
+
if (!l) return false;
|
|
155
|
+
const colonParts = l.split(':');
|
|
156
|
+
let filePath;
|
|
157
|
+
|
|
158
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
159
|
+
// Windows path
|
|
160
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
161
|
+
} else {
|
|
162
|
+
// Unix path
|
|
163
|
+
filePath = colonParts[0];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return filePath && fs.existsSync(filePath);
|
|
167
|
+
})
|
|
109
168
|
|
|
110
169
|
// // filter out 3rd party libs
|
|
111
170
|
.filter(l => !l?.includes(`vendor${sep}`))
|
|
112
171
|
.filter(l => !l?.includes(`node_modules${sep}`))
|
|
113
|
-
.filter(l =>
|
|
114
|
-
|
|
172
|
+
.filter(l => {
|
|
173
|
+
// Extract file path for final check (accounting for Windows drive letters)
|
|
174
|
+
const colonParts = l.split(':');
|
|
175
|
+
let filePath;
|
|
176
|
+
|
|
177
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
178
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
179
|
+
} else {
|
|
180
|
+
filePath = colonParts[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return fs.lstatSync(filePath).isFile();
|
|
184
|
+
});
|
|
115
185
|
|
|
116
186
|
if (!stackLines.length) return '';
|
|
117
187
|
|
|
118
|
-
|
|
188
|
+
// Extract file and line number (accounting for Windows drive letters)
|
|
189
|
+
const firstLine = stackLines[0];
|
|
190
|
+
const colonParts = firstLine.split(':');
|
|
191
|
+
let file, line;
|
|
192
|
+
|
|
193
|
+
if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
194
|
+
// Windows path like D:\path\file.php:24
|
|
195
|
+
file = colonParts[0] + ':' + colonParts[1];
|
|
196
|
+
line = colonParts[2];
|
|
197
|
+
} else {
|
|
198
|
+
// Unix path like /path/file.php:24
|
|
199
|
+
file = colonParts[0];
|
|
200
|
+
line = colonParts[1];
|
|
201
|
+
}
|
|
119
202
|
|
|
120
203
|
const prepend = 3;
|
|
121
204
|
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
@@ -178,30 +261,63 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
178
261
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
179
262
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
180
263
|
} else if (opts.lang === 'csharp') {
|
|
181
|
-
//
|
|
182
|
-
|
|
264
|
+
// Find the method declaration line
|
|
265
|
+
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
|
183
266
|
|
|
184
|
-
if (
|
|
185
|
-
|
|
267
|
+
if (methodLineIndex === -1) {
|
|
268
|
+
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
186
269
|
}
|
|
187
270
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
271
|
+
if (methodLineIndex === -1) {
|
|
272
|
+
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
190
273
|
}
|
|
191
274
|
|
|
192
|
-
//
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
276
|
+
if (methodLineIndex !== -1) {
|
|
277
|
+
lineIndex = methodLineIndex;
|
|
278
|
+
|
|
279
|
+
// Scan upwards to find the start of attributes and comments
|
|
280
|
+
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
281
|
+
const trimmedLine = lines[i].trim();
|
|
282
|
+
|
|
283
|
+
// Include [TestCase], [Test], and other attributes
|
|
284
|
+
if (trimmedLine.startsWith('[')) {
|
|
285
|
+
lineIndex = i;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Include XML documentation comments
|
|
290
|
+
if (trimmedLine.startsWith('///')) {
|
|
291
|
+
lineIndex = i;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Stop at empty lines (with some tolerance)
|
|
296
|
+
if (trimmedLine === '') {
|
|
297
|
+
// Check if next non-empty line is an attribute or comment
|
|
298
|
+
let hasMoreAttributes = false;
|
|
299
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
300
|
+
const nextTrimmed = lines[j].trim();
|
|
301
|
+
if (nextTrimmed === '') continue;
|
|
302
|
+
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
303
|
+
hasMoreAttributes = true;
|
|
304
|
+
lineIndex = j;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (!hasMoreAttributes) break;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Stop at other method declarations or class-level elements
|
|
313
|
+
if (
|
|
314
|
+
trimmedLine.includes('public ') ||
|
|
315
|
+
trimmedLine.includes('private ') ||
|
|
316
|
+
trimmedLine.includes('protected ') ||
|
|
317
|
+
trimmedLine.includes('internal ')
|
|
318
|
+
) {
|
|
319
|
+
if (!trimmedLine.startsWith('[')) break;
|
|
200
320
|
}
|
|
201
|
-
return false;
|
|
202
|
-
});
|
|
203
|
-
if (testAttributeIndex !== -1) {
|
|
204
|
-
lineIndex = testAttributeIndex;
|
|
205
321
|
}
|
|
206
322
|
}
|
|
207
323
|
} else {
|
|
@@ -215,9 +331,29 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
215
331
|
|
|
216
332
|
if (lineIndex !== -1 && lineIndex !== undefined) {
|
|
217
333
|
const result = [];
|
|
334
|
+
let braceDepth = 0; // Track brace depth for C# methods
|
|
335
|
+
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
336
|
+
|
|
218
337
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
219
338
|
if (lines[i] === undefined) continue;
|
|
220
339
|
|
|
340
|
+
// Track brace depth for C# to stop after method closes
|
|
341
|
+
if (opts.lang === 'csharp') {
|
|
342
|
+
const line = lines[i];
|
|
343
|
+
// Count opening and closing braces
|
|
344
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
345
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
346
|
+
|
|
347
|
+
if (openBraces > 0) methodStartFound = true;
|
|
348
|
+
braceDepth += openBraces - closeBraces;
|
|
349
|
+
|
|
350
|
+
// If we've started the method and depth returns to 0, method is complete
|
|
351
|
+
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
352
|
+
// Don't include the closing brace - just break
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
221
357
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
222
358
|
// annotation
|
|
223
359
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
|
|
@@ -238,11 +374,36 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
238
374
|
if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
|
|
239
375
|
if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
|
|
240
376
|
if (opts.lang === 'java' && lines[i].includes(' class ')) break;
|
|
241
|
-
if
|
|
242
|
-
if (opts.lang === 'csharp'
|
|
243
|
-
|
|
244
|
-
|
|
377
|
+
// For C#, additional checks if brace tracking didn't stop us
|
|
378
|
+
if (opts.lang === 'csharp') {
|
|
379
|
+
const trimmed = lines[i].trim();
|
|
380
|
+
// Stop at attribute that marks beginning of next test (but not if we're still in the current method)
|
|
381
|
+
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0) break;
|
|
382
|
+
// Stop at XML documentation comments that belong to next method
|
|
383
|
+
if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0) break;
|
|
384
|
+
// Stop at another method declaration (but not if we're still in the current method)
|
|
385
|
+
if (
|
|
386
|
+
trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
|
|
387
|
+
methodStartFound &&
|
|
388
|
+
braceDepth === 0
|
|
389
|
+
)
|
|
390
|
+
break;
|
|
391
|
+
// Stop at class declaration
|
|
392
|
+
if (trimmed.includes(' class ') && trimmed.includes('public')) break;
|
|
393
|
+
// Stop at helper method calls (like ProcessBooleanValue, AddNumbers) - these are private methods
|
|
394
|
+
if (methodStartFound && trimmed.match(/^\s*\/\/\s*Helper methods for testing/)) break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// For C# tests, stop if we encounter helper method calls in the method body
|
|
399
|
+
if (opts.lang === 'csharp' && methodStartFound && braceDepth > 0) {
|
|
400
|
+
const trimmed = lines[i].trim();
|
|
401
|
+
// Stop at comment indicating helper methods section
|
|
402
|
+
if (trimmed.match(/^\s*\/\/\s*Helper methods for testing/)) {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
245
405
|
}
|
|
406
|
+
|
|
246
407
|
result.push(lines[i]);
|
|
247
408
|
}
|
|
248
409
|
return result.join('\n');
|
|
@@ -454,8 +615,20 @@ function transformEnvVarToBoolean(value) {
|
|
|
454
615
|
return Boolean(value);
|
|
455
616
|
}
|
|
456
617
|
|
|
618
|
+
function truncate(s, size = 255) {
|
|
619
|
+
if (s === undefined || s === null) {
|
|
620
|
+
return '';
|
|
621
|
+
}
|
|
622
|
+
const str = s.toString();
|
|
623
|
+
if (str.trim().length < size) {
|
|
624
|
+
return str;
|
|
625
|
+
}
|
|
626
|
+
return `${str.substring(0, size)}...`;
|
|
627
|
+
}
|
|
628
|
+
|
|
457
629
|
export {
|
|
458
630
|
ansiRegExp,
|
|
631
|
+
truncate,
|
|
459
632
|
cleanLatestRunId,
|
|
460
633
|
isSameTest,
|
|
461
634
|
fetchSourceCode,
|
package/src/xmlReader.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
fetchIdFromCode,
|
|
16
16
|
humanize,
|
|
17
17
|
TEST_ID_REGEX,
|
|
18
|
+
transformEnvVarToBoolean,
|
|
18
19
|
} from './utils/utils.js';
|
|
19
20
|
import { pipesFactory } from './pipe/index.js';
|
|
20
21
|
import adapterFactory from './junit-adapter/index.js';
|
|
@@ -36,6 +37,7 @@ const {
|
|
|
36
37
|
TESTOMATIO_ENV,
|
|
37
38
|
TESTOMATIO_RUN,
|
|
38
39
|
TESTOMATIO_MARK_DETACHED,
|
|
40
|
+
TESTOMATIO_LEGACY_NUNIT,
|
|
39
41
|
} = process.env;
|
|
40
42
|
|
|
41
43
|
const options = {
|
|
@@ -77,7 +79,8 @@ class XmlReader {
|
|
|
77
79
|
this.uploader = new S3Uploader();
|
|
78
80
|
|
|
79
81
|
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
80
|
-
|
|
82
|
+
// Can be disabled via opts.enhancedNunit = false or TESTOMATIO_LEGACY_NUNIT=1
|
|
83
|
+
this.enhancedNunit = opts.enhancedNunit !== false && !transformEnvVarToBoolean(TESTOMATIO_LEGACY_NUNIT); // Default true, can be disabled
|
|
81
84
|
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
82
85
|
|
|
83
86
|
// @ts-ignore
|
|
@@ -240,59 +243,20 @@ class XmlReader {
|
|
|
240
243
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
241
244
|
if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
|
|
242
245
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
246
|
-
let example = td.name.match(/\((.*?)\)/);
|
|
247
|
-
if (example) example = { ...example[1].split(',') };
|
|
248
|
-
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
249
|
-
const suite_title = suite.pop();
|
|
250
|
-
return {
|
|
251
|
-
title,
|
|
252
|
-
example,
|
|
253
|
-
file: suite.join('/'),
|
|
254
|
-
description: td.Description,
|
|
255
|
-
suite_title,
|
|
256
|
-
id: td.Execution.id,
|
|
257
|
-
};
|
|
258
|
-
}) || [];
|
|
246
|
+
// Parse test definitions
|
|
247
|
+
const tests = defs.map(td => this._parseTRXTestDefinition(td));
|
|
259
248
|
|
|
249
|
+
// Parse test results
|
|
260
250
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
261
251
|
if (!Array.isArray(result)) result = [result].filter(d => !!d);
|
|
262
252
|
|
|
263
|
-
const results = result.map(td => (
|
|
264
|
-
id: td.executionId,
|
|
265
|
-
// seconds are used in junit reports, but ms are used by testomatio
|
|
266
|
-
run_time: parseFloat(td.duration) * 1000,
|
|
267
|
-
status: td.outcome,
|
|
268
|
-
stack: td.Output.StdOut,
|
|
269
|
-
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
results.forEach(r => {
|
|
273
|
-
const test = tests.find(t => t.id === r.id) || {};
|
|
274
|
-
r.suite_title = test.suite_title;
|
|
275
|
-
r.title = test.title?.trim();
|
|
276
|
-
if (test.code) r.code = test.code;
|
|
277
|
-
if (test.description) r.description = test.description;
|
|
278
|
-
if (test.example) r.example = test.example;
|
|
279
|
-
if (test.file) r.file = test.file;
|
|
280
|
-
r.create = true;
|
|
281
|
-
r.overwrite = true;
|
|
282
|
-
if (r.status === 'Passed') r.status = STATUS.PASSED;
|
|
283
|
-
if (r.status === 'Failed') r.status = STATUS.FAILED;
|
|
284
|
-
if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
|
|
285
|
-
delete r.id;
|
|
286
|
-
});
|
|
253
|
+
const results = result.map(td => this._parseTRXTestResult(td, tests));
|
|
287
254
|
|
|
288
255
|
debug(results);
|
|
289
256
|
|
|
290
257
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
291
|
-
|
|
292
258
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
293
|
-
|
|
294
|
-
let status = STATUS.PASSED.toString();
|
|
295
|
-
if (failed_count > 0) status = STATUS.FAILED;
|
|
259
|
+
const status = failed_count > 0 ? STATUS.FAILED : STATUS.PASSED.toString();
|
|
296
260
|
|
|
297
261
|
this.tests = results.filter(t => !!t.title);
|
|
298
262
|
|
|
@@ -307,6 +271,67 @@ class XmlReader {
|
|
|
307
271
|
};
|
|
308
272
|
}
|
|
309
273
|
|
|
274
|
+
_parseTRXTestDefinition(td) {
|
|
275
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
276
|
+
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
277
|
+
const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
|
|
278
|
+
|
|
279
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
280
|
+
const suite_title = suite.pop();
|
|
281
|
+
|
|
282
|
+
// Convert namespace to file path for C#
|
|
283
|
+
const file = `${suite.join('/')}.cs`;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
title, // Base name without parameters for test import
|
|
287
|
+
example, // Parameters object for parameterized tests
|
|
288
|
+
file, // File path with .cs extension
|
|
289
|
+
description: td.Description,
|
|
290
|
+
suite_title,
|
|
291
|
+
id: td.Execution.id,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_parseTRXTestResult(td, tests) {
|
|
296
|
+
const test = tests.find(t => t.id === td.executionId) || {};
|
|
297
|
+
|
|
298
|
+
const result = {
|
|
299
|
+
suite_title: test.suite_title,
|
|
300
|
+
title: test.title?.trim(),
|
|
301
|
+
file: test.file,
|
|
302
|
+
description: test.description,
|
|
303
|
+
code: test.code,
|
|
304
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
305
|
+
stack: td.Output?.StdOut || '',
|
|
306
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
307
|
+
create: true,
|
|
308
|
+
overwrite: true,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Add example for parameterized tests
|
|
312
|
+
if (test.example) {
|
|
313
|
+
result.example = test.example;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Map TRX status to Testomat.io status
|
|
317
|
+
result.status = this._mapTRXStatus(td.outcome);
|
|
318
|
+
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_mapTRXStatus(outcome) {
|
|
323
|
+
switch (outcome) {
|
|
324
|
+
case 'Passed':
|
|
325
|
+
return STATUS.PASSED;
|
|
326
|
+
case 'Failed':
|
|
327
|
+
return STATUS.FAILED;
|
|
328
|
+
case 'Skipped':
|
|
329
|
+
return STATUS.SKIPPED;
|
|
330
|
+
default:
|
|
331
|
+
return STATUS.PASSED;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
310
335
|
processXUnit(assemblies) {
|
|
311
336
|
const tests = [];
|
|
312
337
|
|