@testomatio/reporter 2.3.7-beta.3-xml-import → 2.3.7-beta.4-stack-artifacts

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.
@@ -1,369 +0,0 @@
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.NUnitXmlParser = void 0;
7
- const debug_1 = __importDefault(require("debug"));
8
- const constants_js_1 = require("../constants.js");
9
- const debug = (0, debug_1.default)('@testomatio/reporter:nunit-parser');
10
- /**
11
- * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
12
- * and parameterized tests
13
- */
14
- class NUnitXmlParser {
15
- constructor(options = {}) {
16
- this.options = options;
17
- this.tests = [];
18
- this.stats = {
19
- total: 0,
20
- passed: 0,
21
- failed: 0,
22
- skipped: 0,
23
- inconclusive: 0,
24
- };
25
- }
26
- /**
27
- * Parse NUnit XML test-run structure
28
- * @param {Object} testRun - Parsed XML test-run object
29
- * @returns {Object} - Parsed test results
30
- */
31
- parseTestRun(testRun) {
32
- debug('Parsing NUnit test-run');
33
- // Extract run-level statistics
34
- this.stats = {
35
- total: parseInt(testRun.total || 0, 10),
36
- passed: parseInt(testRun.passed || 0, 10),
37
- failed: parseInt(testRun.failed || 0, 10),
38
- skipped: parseInt(testRun.skipped || 0, 10),
39
- inconclusive: parseInt(testRun.inconclusive || 0, 10),
40
- };
41
- // Process the root test-suite
42
- if (testRun['test-suite']) {
43
- this.parseTestSuite(testRun['test-suite'], []);
44
- }
45
- debug(`Parsed ${this.tests.length} tests from NUnit XML`);
46
- return {
47
- status: testRun.result?.toLowerCase() || 'unknown',
48
- create_tests: true,
49
- tests_count: this.tests.length,
50
- passed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.PASSED).length,
51
- failed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length,
52
- skipped_count: this.tests.filter(t => t.status === constants_js_1.STATUS.SKIPPED).length,
53
- tests: this.tests,
54
- };
55
- }
56
- /**
57
- * Recursively parse test-suite elements based on their type
58
- * @param {Object|Array} testSuite - Test suite object or array
59
- * @param {Array} parentPath - Current path in the hierarchy
60
- */
61
- parseTestSuite(testSuite, parentPath = []) {
62
- if (!testSuite)
63
- return;
64
- // Handle arrays of test suites
65
- if (Array.isArray(testSuite)) {
66
- testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
67
- return;
68
- }
69
- const suiteType = testSuite.type;
70
- const suiteName = testSuite.name;
71
- const fullName = testSuite.fullname;
72
- debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
73
- switch (suiteType) {
74
- case 'Assembly':
75
- // Assembly level - ignore the name, just process children
76
- debug('Processing Assembly level - ignoring name, processing children');
77
- this.processChildren(testSuite, parentPath);
78
- break;
79
- case 'TestSuite':
80
- // Namespace/grouping level - add to path but don't create test
81
- debug(`Processing TestSuite level - adding '${suiteName}' to path`);
82
- const newPath = [...parentPath, suiteName];
83
- this.processChildren(testSuite, newPath);
84
- break;
85
- case 'TestFixture':
86
- // Test class level - add to path and process test cases
87
- debug(`Processing TestFixture level - test class '${suiteName}'`);
88
- const testFixturePath = [...parentPath, suiteName];
89
- this.processChildren(testSuite, testFixturePath);
90
- break;
91
- default:
92
- debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
93
- const unknownPath = [...parentPath, suiteName];
94
- this.processChildren(testSuite, unknownPath);
95
- break;
96
- }
97
- }
98
- /**
99
- * Process child elements of a test suite
100
- * @param {Object} testSuite - Test suite object
101
- * @param {Array} currentPath - Current path in hierarchy
102
- */
103
- processChildren(testSuite, currentPath) {
104
- // Process nested test-suites
105
- if (testSuite['test-suite']) {
106
- this.parseTestSuite(testSuite['test-suite'], currentPath);
107
- }
108
- // Process test-cases
109
- if (testSuite['test-case']) {
110
- this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
111
- }
112
- }
113
- /**
114
- * Parse test-case elements (actual tests)
115
- * @param {Object|Array} testCases - Test case object or array
116
- * @param {Array} suitePath - Path to the test suite
117
- * @param {Object} parentSuite - Parent test suite for context
118
- */
119
- parseTestCases(testCases, suitePath, parentSuite) {
120
- if (!testCases)
121
- return;
122
- // Handle arrays of test cases
123
- if (!Array.isArray(testCases)) {
124
- testCases = [testCases];
125
- }
126
- testCases.forEach(testCase => {
127
- const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
128
- if (parsedTest) {
129
- this.tests.push(parsedTest);
130
- }
131
- });
132
- }
133
- /**
134
- * Parse individual test case
135
- * @param {Object} testCase - Test case object
136
- * @param {Array} suitePath - Path to the test suite
137
- * @param {Object} parentSuite - Parent test suite for context
138
- * @returns {Object|null} - Parsed test object
139
- */
140
- parseTestCase(testCase, suitePath, parentSuite) {
141
- if (!testCase || !testCase.name) {
142
- debug('Skipping test case without name');
143
- return null;
144
- }
145
- const testName = testCase.name;
146
- const fullName = testCase.fullname;
147
- const methodName = testCase.methodname || this.extractMethodName(testName);
148
- const className = testCase.classname || parentSuite?.name;
149
- debug(`Parsing test case: ${testName}`);
150
- // Extract parameters if this is a parameterized test
151
- const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
152
- // Determine test status
153
- let status = constants_js_1.STATUS.PASSED;
154
- if (testCase.result) {
155
- switch (testCase.result.toLowerCase()) {
156
- case 'passed':
157
- status = constants_js_1.STATUS.PASSED;
158
- break;
159
- case 'failed':
160
- status = constants_js_1.STATUS.FAILED;
161
- break;
162
- case 'skipped':
163
- case 'ignored':
164
- status = constants_js_1.STATUS.SKIPPED;
165
- break;
166
- case 'inconclusive':
167
- status = constants_js_1.STATUS.SKIPPED; // Treat inconclusive as skipped
168
- break;
169
- default:
170
- status = constants_js_1.STATUS.PASSED;
171
- }
172
- }
173
- // Extract error information
174
- let message = '';
175
- let stack = '';
176
- if (testCase.failure) {
177
- message = testCase.failure.message || '';
178
- stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
179
- }
180
- if (testCase.output && testCase.output['#text']) {
181
- stack = `${stack}\n\n${testCase.output['#text']}`.trim();
182
- }
183
- // Extract test ID from properties
184
- let testId = null;
185
- if (testCase.properties && testCase.properties.property) {
186
- const properties = Array.isArray(testCase.properties.property)
187
- ? testCase.properties.property
188
- : [testCase.properties.property];
189
- const idProperty = properties.find(p => p.name === 'ID');
190
- if (idProperty) {
191
- testId = idProperty.value;
192
- // Remove @ and T prefixes if present
193
- if (testId.startsWith('@'))
194
- testId = testId.slice(1);
195
- if (testId.startsWith('T'))
196
- testId = testId.slice(1);
197
- }
198
- }
199
- // Build file path from suite path and class name
200
- const filePath = this.buildFilePath(suitePath, className, parentSuite);
201
- // For parameterized tests, format example as expected by Testomatio API
202
- // Convert array of parameters to object with numeric keys
203
- let example = null;
204
- if (isParameterized && parameters.length > 0) {
205
- example = {};
206
- parameters.forEach((param, index) => {
207
- example[index] = param;
208
- });
209
- }
210
- return {
211
- // For runs: use full test name with parameters (TestBooleanValue(true))
212
- // For import: API will group by base name using the example field
213
- title: testName, // Full name with parameters for run display
214
- methodName: baseMethodName || methodName || testName,
215
- fullName: fullName,
216
- suitePath: suitePath,
217
- suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
218
- file: filePath,
219
- status: status,
220
- message: message,
221
- stack: stack,
222
- run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
223
- test_id: testId,
224
- create: true,
225
- retry: false,
226
- // Parameterized test metadata
227
- example: example, // Parameters as object for API grouping
228
- isParameterized: isParameterized,
229
- parameters: parameters, // Keep original array for reference
230
- baseMethodName: baseMethodName,
231
- };
232
- }
233
- /**
234
- * Extract method name and parameters from test name
235
- * @param {string} testName - Full test name
236
- * @returns {Object} - Extracted information
237
- */
238
- extractParameters(testName) {
239
- const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
240
- if (paramMatch) {
241
- const baseMethodName = paramMatch[1].trim();
242
- const paramString = paramMatch[2];
243
- // Parse parameters - handle quoted strings and nested structures
244
- const parameters = this.parseParameterString(paramString);
245
- return {
246
- baseMethodName: baseMethodName,
247
- parameters: parameters,
248
- isParameterized: true,
249
- };
250
- }
251
- return {
252
- baseMethodName: testName,
253
- parameters: [],
254
- isParameterized: false,
255
- };
256
- }
257
- /**
258
- * Parse parameter string into array of parameters
259
- * @param {string} paramString - Parameter string
260
- * @returns {Array} - Array of parameters
261
- */
262
- parseParameterString(paramString) {
263
- const parameters = [];
264
- let current = '';
265
- let inQuotes = false;
266
- let quoteChar = null;
267
- let depth = 0;
268
- for (let i = 0; i < paramString.length; i++) {
269
- const char = paramString[i];
270
- if (!inQuotes && (char === '"' || char === "'")) {
271
- inQuotes = true;
272
- quoteChar = char;
273
- current += char;
274
- }
275
- else if (inQuotes && char === quoteChar) {
276
- inQuotes = false;
277
- quoteChar = null;
278
- current += char;
279
- }
280
- else if (!inQuotes && char === '(') {
281
- depth++;
282
- current += char;
283
- }
284
- else if (!inQuotes && char === ')') {
285
- depth--;
286
- current += char;
287
- }
288
- else if (!inQuotes && char === ',' && depth === 0) {
289
- parameters.push(current.trim());
290
- current = '';
291
- }
292
- else {
293
- current += char;
294
- }
295
- }
296
- if (current.trim()) {
297
- parameters.push(current.trim());
298
- }
299
- // Clean up parameters - remove quotes if they wrap the entire parameter
300
- return parameters.map(param => {
301
- param = param.trim();
302
- if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
303
- return param.slice(1, -1);
304
- }
305
- return param;
306
- });
307
- }
308
- /**
309
- * Extract method name from test name (fallback)
310
- * @param {string} testName - Test name
311
- * @returns {string} - Method name
312
- */
313
- extractMethodName(testName) {
314
- // Remove parameters if present
315
- const paramMatch = testName.match(/^(.+?)\(/);
316
- return paramMatch ? paramMatch[1].trim() : testName;
317
- }
318
- /**
319
- * Build file path from suite path and class name
320
- * @param {Array} suitePath - Suite path array
321
- * @param {string} className - Class name
322
- * @param {Object} parentSuite - Parent suite for context
323
- * @returns {string} - File path
324
- */
325
- buildFilePath(suitePath, className, parentSuite) {
326
- // Try to get file path from parent suite
327
- if (parentSuite && parentSuite.filepath) {
328
- return parentSuite.filepath;
329
- }
330
- // Build path from suite hierarchy
331
- const pathParts = [...suitePath];
332
- if (className && !pathParts.includes(className)) {
333
- pathParts.push(className);
334
- }
335
- // Convert to file path format
336
- return pathParts.join('/') + '.cs'; // Assume C# for NUnit
337
- }
338
- /**
339
- * Group parameterized tests by base method name
340
- * @param {Array} tests - Array of parsed tests
341
- * @returns {Object} - Grouped tests
342
- */
343
- groupParameterizedTests(tests) {
344
- const grouped = {};
345
- tests.forEach(test => {
346
- const key = test.isParameterized
347
- ? `${test.suitePath.join('.')}.${test.baseMethodName}`
348
- : `${test.suitePath.join('.')}.${test.title}`;
349
- if (!grouped[key]) {
350
- grouped[key] = {
351
- baseTest: {
352
- name: test.baseMethodName || test.title,
353
- suitePath: test.suitePath,
354
- suite_title: test.suite_title,
355
- file: test.file,
356
- isParameterized: test.isParameterized,
357
- },
358
- variations: [],
359
- };
360
- }
361
- grouped[key].variations.push(test);
362
- });
363
- return grouped;
364
- }
365
- }
366
- exports.NUnitXmlParser = NUnitXmlParser;
367
- module.exports = NUnitXmlParser;
368
-
369
- module.exports.NUnitXmlParser = NUnitXmlParser;