@testomatio/reporter 2.3.8 → 2.3.9-beta-bin-fix
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/README.md +1 -2
- package/lib/bin/cli.js +4 -14
- package/lib/bin/reportXml.js +2 -5
- package/lib/client.d.ts +1 -1
- package/lib/client.js +22 -31
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -40
- package/lib/pipe/testomatio.d.ts +1 -2
- package/lib/pipe/testomatio.js +1 -2
- package/lib/reporter.d.ts +9 -19
- package/lib/reporter.js +5 -40
- package/lib/uploader.js +0 -4
- package/lib/utils/utils.js +24 -189
- package/lib/xmlReader.d.ts +26 -32
- package/lib/xmlReader.js +52 -111
- package/package.json +5 -9
- package/src/bin/cli.js +4 -16
- package/src/bin/reportXml.js +2 -5
- package/src/client.js +28 -56
- package/src/junit-adapter/csharp.js +6 -45
- package/src/pipe/testomatio.js +1 -2
- package/src/reporter.js +4 -7
- package/src/uploader.js +0 -5
- package/src/utils/utils.js +22 -202
- package/src/xmlReader.js +46 -134
- package/lib/junit-adapter/nunit-parser.d.ts +0 -82
- package/lib/junit-adapter/nunit-parser.js +0 -431
- package/src/junit-adapter/nunit-parser.js +0 -472
- package/types/types.d.ts +0 -364
- package/types/vitest.types.d.ts +0 -93
package/lib/utils/utils.js
CHANGED
|
@@ -116,26 +116,19 @@ const isValidUrl = s => {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
exports.isValidUrl = isValidUrl;
|
|
119
|
-
const fileMatchRegex = /file:(
|
|
119
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
120
120
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
121
|
-
|
|
122
|
-
.map(
|
|
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())
|
|
121
|
+
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
122
|
+
.map(f => f[1].trim())
|
|
130
123
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
131
124
|
.map(f => {
|
|
132
|
-
//
|
|
133
|
-
|
|
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;
|
|
134
131
|
});
|
|
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
|
-
}
|
|
139
132
|
debug('Found files in stack trace: ', files);
|
|
140
133
|
return files.filter(f => {
|
|
141
134
|
if (!checkExists)
|
|
@@ -151,88 +144,19 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
151
144
|
const stackLines = stack
|
|
152
145
|
.split('\n')
|
|
153
146
|
.filter(l => l.includes(':'))
|
|
147
|
+
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
148
|
+
// .map(l => l.split(':')[0])
|
|
154
149
|
.map(l => l.trim())
|
|
155
|
-
.map(l =>
|
|
156
|
-
|
|
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
|
-
})
|
|
150
|
+
.map(l => l.split(' ').find(p => p.includes(':')) || '')
|
|
151
|
+
.filter(l => (0, is_valid_path_1.default)(l?.split(':')[0]))
|
|
205
152
|
// // filter out 3rd party libs
|
|
206
153
|
.filter(l => !l?.includes(`vendor${path_1.sep}`))
|
|
207
154
|
.filter(l => !l?.includes(`node_modules${path_1.sep}`))
|
|
208
|
-
.filter(l =>
|
|
209
|
-
|
|
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
|
-
});
|
|
155
|
+
.filter(l => fs_1.default.existsSync(l.split(':')[0]))
|
|
156
|
+
.filter(l => fs_1.default.lstatSync(l.split(':')[0]).isFile());
|
|
220
157
|
if (!stackLines.length)
|
|
221
158
|
return '';
|
|
222
|
-
|
|
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
|
-
}
|
|
159
|
+
const [file, line] = stackLines[0].split(':');
|
|
236
160
|
const prepend = 3;
|
|
237
161
|
const source = fetchSourceCode(fs_1.default.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
238
162
|
if (!source)
|
|
@@ -250,8 +174,6 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
|
|
250
174
|
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
251
175
|
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
252
176
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
253
|
-
if (!code)
|
|
254
|
-
return null;
|
|
255
177
|
const comments = code
|
|
256
178
|
.split('\n')
|
|
257
179
|
.map(l => l.trim())
|
|
@@ -294,58 +216,10 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
294
216
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
295
217
|
}
|
|
296
218
|
else if (opts.lang === 'csharp') {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
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
|
-
}
|
|
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}(`));
|
|
349
223
|
}
|
|
350
224
|
else {
|
|
351
225
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -354,28 +228,11 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
354
228
|
if (opts.prepend) {
|
|
355
229
|
lineIndex -= opts.prepend;
|
|
356
230
|
}
|
|
357
|
-
if (lineIndex
|
|
231
|
+
if (lineIndex) {
|
|
358
232
|
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
|
|
361
233
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
362
234
|
if (lines[i] === undefined)
|
|
363
235
|
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
|
-
}
|
|
379
236
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
380
237
|
// annotation
|
|
381
238
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
|
|
@@ -414,24 +271,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
414
271
|
break;
|
|
415
272
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
416
273
|
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
|
-
}
|
|
435
274
|
}
|
|
436
275
|
result.push(lines[i]);
|
|
437
276
|
}
|
|
@@ -632,14 +471,10 @@ function transformEnvVarToBoolean(value) {
|
|
|
632
471
|
return Boolean(value);
|
|
633
472
|
}
|
|
634
473
|
function truncate(s, size = 255) {
|
|
635
|
-
if (s
|
|
636
|
-
return
|
|
637
|
-
}
|
|
638
|
-
const str = s.toString();
|
|
639
|
-
if (str.trim().length < size) {
|
|
640
|
-
return str;
|
|
474
|
+
if (s.toString().trim().length < size) {
|
|
475
|
+
return s.toString();
|
|
641
476
|
}
|
|
642
|
-
return `${
|
|
477
|
+
return `${s.toString().substring(0, size)}...`;
|
|
643
478
|
}
|
|
644
479
|
|
|
645
480
|
module.exports.getPackageVersion = getPackageVersion;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -19,11 +19,25 @@ declare class XmlReader {
|
|
|
19
19
|
tests: any[];
|
|
20
20
|
stats: {};
|
|
21
21
|
uploader: S3Uploader;
|
|
22
|
-
enhancedNunit: boolean;
|
|
23
|
-
groupParameterized: boolean;
|
|
24
22
|
version: any;
|
|
25
23
|
connectAdapter(): import("./junit-adapter/adapter.js").default;
|
|
26
|
-
parse(fileName: any):
|
|
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
|
+
};
|
|
27
41
|
processJUnit(jsonSuite: any): {
|
|
28
42
|
create_tests: boolean;
|
|
29
43
|
duration: number;
|
|
@@ -35,14 +49,15 @@ declare class XmlReader {
|
|
|
35
49
|
tests: any[];
|
|
36
50
|
tests_count: number;
|
|
37
51
|
};
|
|
38
|
-
processNUnit(jsonSuite: any):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
};
|
|
46
61
|
processTRX(jsonSuite: any): {
|
|
47
62
|
status: string;
|
|
48
63
|
create_tests: boolean;
|
|
@@ -52,27 +67,6 @@ declare class XmlReader {
|
|
|
52
67
|
failed_count: number;
|
|
53
68
|
tests: any;
|
|
54
69
|
};
|
|
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;
|
|
76
70
|
processXUnit(assemblies: any): {
|
|
77
71
|
status: string;
|
|
78
72
|
create_tests: boolean;
|
package/lib/xmlReader.js
CHANGED
|
@@ -11,7 +11,6 @@ const fast_xml_parser_1 = require("fast-xml-parser");
|
|
|
11
11
|
const constants_js_1 = require("./constants.js");
|
|
12
12
|
const crypto_1 = require("crypto");
|
|
13
13
|
const url_1 = require("url");
|
|
14
|
-
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
15
14
|
const utils_js_1 = require("./utils/utils.js");
|
|
16
15
|
const index_js_1 = require("./pipe/index.js");
|
|
17
16
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
@@ -21,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
|
|
|
21
20
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
22
21
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
23
22
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
24
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED,
|
|
23
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
|
|
25
24
|
const options = {
|
|
26
25
|
ignoreDeclaration: true,
|
|
27
26
|
ignoreAttributes: false,
|
|
@@ -55,10 +54,6 @@ class XmlReader {
|
|
|
55
54
|
this.stats = {};
|
|
56
55
|
this.stats.language = opts.lang?.toLowerCase();
|
|
57
56
|
this.uploader = new uploader_js_1.S3Uploader();
|
|
58
|
-
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
59
|
-
// Can be disabled via opts.enhancedNunit = false or TESTOMATIO_LEGACY_NUNIT=1
|
|
60
|
-
this.enhancedNunit = !(0, utils_js_1.transformEnvVarToBoolean)(TESTOMATIO_LEGACY_NUNIT);
|
|
61
|
-
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
62
57
|
// @ts-ignore
|
|
63
58
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
64
59
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
@@ -107,8 +102,7 @@ class XmlReader {
|
|
|
107
102
|
return this.processJUnit(jsonSuite);
|
|
108
103
|
}
|
|
109
104
|
processJUnit(jsonSuite) {
|
|
110
|
-
const { testsuite, name, failures, errors } = jsonSuite;
|
|
111
|
-
const tests = testsuite?.tests || jsonSuite.tests;
|
|
105
|
+
const { testsuite, name, tests, failures, errors } = jsonSuite;
|
|
112
106
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
113
107
|
const resultTests = processTestSuite(testsuite);
|
|
114
108
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
@@ -134,13 +128,6 @@ class XmlReader {
|
|
|
134
128
|
};
|
|
135
129
|
}
|
|
136
130
|
processNUnit(jsonSuite) {
|
|
137
|
-
// Use enhanced NUnit parser if enabled and this is actually NUnit XML
|
|
138
|
-
if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
|
|
139
|
-
debug('Using enhanced NUnit parser');
|
|
140
|
-
return this.processNUnitEnhanced(jsonSuite);
|
|
141
|
-
}
|
|
142
|
-
// Fallback to legacy parser for backward compatibility
|
|
143
|
-
debug('Using legacy NUnit parser');
|
|
144
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
145
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
146
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
@@ -155,58 +142,65 @@ class XmlReader {
|
|
|
155
142
|
tests: resultTests,
|
|
156
143
|
};
|
|
157
144
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
160
|
-
* @param {Object} jsonSuite - Parsed XML suite object
|
|
161
|
-
* @returns {boolean} - True if this is NUnit XML format
|
|
162
|
-
*/
|
|
163
|
-
isNUnitXml(jsonSuite) {
|
|
164
|
-
// NUnit XML has test-suite elements with type attributes
|
|
165
|
-
if (jsonSuite['test-suite']) {
|
|
166
|
-
const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
|
|
167
|
-
// Check for NUnit-specific test-suite types
|
|
168
|
-
return (testSuite &&
|
|
169
|
-
testSuite.type &&
|
|
170
|
-
['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
|
|
171
|
-
}
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
processNUnitEnhanced(jsonSuite) {
|
|
175
|
-
debug('Processing NUnit XML with enhanced parser');
|
|
176
|
-
try {
|
|
177
|
-
const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
|
|
178
|
-
groupParameterized: this.groupParameterized,
|
|
179
|
-
...this.opts,
|
|
180
|
-
});
|
|
181
|
-
const result = nunitParser.parseTestRun(jsonSuite);
|
|
182
|
-
// Add parsed tests to our collection
|
|
183
|
-
this.tests = this.tests.concat(result.tests);
|
|
184
|
-
debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
|
|
189
|
-
console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
|
|
190
|
-
// Fallback to legacy parser
|
|
191
|
-
this.enhancedNunit = false;
|
|
192
|
-
return this.processNUnit(jsonSuite);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
145
|
processTRX(jsonSuite) {
|
|
196
146
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
197
147
|
if (!Array.isArray(defs))
|
|
198
148
|
defs = [defs].filter(d => !!d);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
149
|
+
const tests = defs.map(td => {
|
|
150
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
151
|
+
let example = td.name.match(/\((.*?)\)/);
|
|
152
|
+
if (example)
|
|
153
|
+
example = { ...example[1].split(',') };
|
|
154
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
155
|
+
const suite_title = suite.pop();
|
|
156
|
+
return {
|
|
157
|
+
title,
|
|
158
|
+
example,
|
|
159
|
+
file: suite.join('/'),
|
|
160
|
+
description: td.Description,
|
|
161
|
+
suite_title,
|
|
162
|
+
id: td.Execution.id,
|
|
163
|
+
};
|
|
164
|
+
}) || [];
|
|
202
165
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
203
166
|
if (!Array.isArray(result))
|
|
204
167
|
result = [result].filter(d => !!d);
|
|
205
|
-
const results = result.map(td =>
|
|
168
|
+
const results = result.map(td => ({
|
|
169
|
+
id: td.executionId,
|
|
170
|
+
// seconds are used in junit reports, but ms are used by testomatio
|
|
171
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
172
|
+
status: td.outcome,
|
|
173
|
+
stack: td.Output.StdOut,
|
|
174
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
175
|
+
}));
|
|
176
|
+
results.forEach(r => {
|
|
177
|
+
const test = tests.find(t => t.id === r.id) || {};
|
|
178
|
+
r.suite_title = test.suite_title;
|
|
179
|
+
r.title = test.title?.trim();
|
|
180
|
+
if (test.code)
|
|
181
|
+
r.code = test.code;
|
|
182
|
+
if (test.description)
|
|
183
|
+
r.description = test.description;
|
|
184
|
+
if (test.example)
|
|
185
|
+
r.example = test.example;
|
|
186
|
+
if (test.file)
|
|
187
|
+
r.file = test.file;
|
|
188
|
+
r.create = true;
|
|
189
|
+
r.overwrite = true;
|
|
190
|
+
if (r.status === 'Passed')
|
|
191
|
+
r.status = constants_js_1.STATUS.PASSED;
|
|
192
|
+
if (r.status === 'Failed')
|
|
193
|
+
r.status = constants_js_1.STATUS.FAILED;
|
|
194
|
+
if (r.status === 'Skipped')
|
|
195
|
+
r.status = constants_js_1.STATUS.SKIPPED;
|
|
196
|
+
delete r.id;
|
|
197
|
+
});
|
|
206
198
|
debug(results);
|
|
207
199
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
208
200
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
209
|
-
|
|
201
|
+
let status = constants_js_1.STATUS.PASSED.toString();
|
|
202
|
+
if (failed_count > 0)
|
|
203
|
+
status = constants_js_1.STATUS.FAILED;
|
|
210
204
|
this.tests = results.filter(t => !!t.title);
|
|
211
205
|
return {
|
|
212
206
|
status,
|
|
@@ -218,59 +212,6 @@ class XmlReader {
|
|
|
218
212
|
tests: results,
|
|
219
213
|
};
|
|
220
214
|
}
|
|
221
|
-
_parseTRXTestDefinition(td) {
|
|
222
|
-
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
223
|
-
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
224
|
-
const example = exampleMatch ? {
|
|
225
|
-
...exampleMatch[1].split(',').map(p => p.trim()).filter(p => p !== '')
|
|
226
|
-
} : null;
|
|
227
|
-
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
228
|
-
const suite_title = suite.pop();
|
|
229
|
-
// Convert namespace to file path for C#
|
|
230
|
-
const file = `${suite.join('/')}.cs`;
|
|
231
|
-
return {
|
|
232
|
-
title, // Base name without parameters for test import
|
|
233
|
-
example, // Parameters object for parameterized tests
|
|
234
|
-
file, // File path with .cs extension
|
|
235
|
-
description: td.Description,
|
|
236
|
-
suite_title,
|
|
237
|
-
id: td.Execution.id,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
_parseTRXTestResult(td, tests) {
|
|
241
|
-
const test = tests.find(t => t.id === td.executionId) || {};
|
|
242
|
-
const result = {
|
|
243
|
-
suite_title: test.suite_title,
|
|
244
|
-
title: test.title?.trim(),
|
|
245
|
-
file: test.file,
|
|
246
|
-
description: test.description,
|
|
247
|
-
code: test.code,
|
|
248
|
-
run_time: parseFloat(td.duration) * 1000,
|
|
249
|
-
stack: td.Output?.StdOut || '',
|
|
250
|
-
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
251
|
-
create: true,
|
|
252
|
-
overwrite: true,
|
|
253
|
-
};
|
|
254
|
-
// Add example for parameterized tests
|
|
255
|
-
if (test.example) {
|
|
256
|
-
result.example = test.example;
|
|
257
|
-
}
|
|
258
|
-
// Map TRX status to Testomat.io status
|
|
259
|
-
result.status = this._mapTRXStatus(td.outcome);
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
_mapTRXStatus(outcome) {
|
|
263
|
-
switch (outcome) {
|
|
264
|
-
case 'Passed':
|
|
265
|
-
return constants_js_1.STATUS.PASSED;
|
|
266
|
-
case 'Failed':
|
|
267
|
-
return constants_js_1.STATUS.FAILED;
|
|
268
|
-
case 'Skipped':
|
|
269
|
-
return constants_js_1.STATUS.SKIPPED;
|
|
270
|
-
default:
|
|
271
|
-
return constants_js_1.STATUS.PASSED;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
215
|
processXUnit(assemblies) {
|
|
275
216
|
const tests = [];
|
|
276
217
|
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
|
@@ -518,7 +459,7 @@ function reduceTestCases(prev, item) {
|
|
|
518
459
|
tags ||= [];
|
|
519
460
|
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
520
461
|
if (exampleMatches) {
|
|
521
|
-
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, ''))
|
|
462
|
+
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
522
463
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
523
464
|
}
|
|
524
465
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9-beta-bin-fix",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
9
|
-
"module": "src/reporter.js",
|
|
10
|
-
"types": "types/types.d.ts",
|
|
8
|
+
"typings": "typings/index.d.ts",
|
|
11
9
|
"repository": "git@github.com:testomatio/reporter.git",
|
|
12
10
|
"author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
|
|
13
11
|
"license": "MIT",
|
|
@@ -47,8 +45,7 @@
|
|
|
47
45
|
"bin",
|
|
48
46
|
"lib",
|
|
49
47
|
"src",
|
|
50
|
-
"testcafe"
|
|
51
|
-
"types"
|
|
48
|
+
"testcafe"
|
|
52
49
|
],
|
|
53
50
|
"scripts": {
|
|
54
51
|
"clear-exportdir": "rm -rf export/",
|
|
@@ -60,8 +57,7 @@
|
|
|
60
57
|
"test": "mocha 'tests/unit/**/*_test.js'",
|
|
61
58
|
"test:playwright": "mocha tests/adapter/playwright.test.js",
|
|
62
59
|
"test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
|
|
63
|
-
"test:
|
|
64
|
-
"test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest",
|
|
60
|
+
"test:frameworks": "npm run test:playwright && npm run test:codecept",
|
|
65
61
|
"test:all": "npm run test && npm run test:frameworks",
|
|
66
62
|
"test:adapters": "mocha tests/adapter/*.test.js",
|
|
67
63
|
"test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
|
|
@@ -106,7 +102,7 @@
|
|
|
106
102
|
"vitest": "^1.6.0"
|
|
107
103
|
},
|
|
108
104
|
"bin": {
|
|
109
|
-
"
|
|
105
|
+
"testomatio/reporter": "./lib/bin/cli.js",
|
|
110
106
|
"report-xml": "./lib/bin/reportXml.js",
|
|
111
107
|
"start-test-run": "./lib/bin/startTest.js",
|
|
112
108
|
"upload-artifacts": "./lib/bin/uploadArtifacts.js"
|