@testomatio/reporter 2.1.3-beta.2-xml-import → 2.1.3-beta.2-multi-links

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.
@@ -41,7 +41,7 @@ class DataStorage {
41
41
  /**
42
42
  * Puts any data to storage (file or global variable).
43
43
  * If file: stores data as text, if global variable – stores as array of data.
44
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
44
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
45
45
  * @param {*} data anything you want to store (string, object, array, etc)
46
46
  * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
47
47
  * suite name + test name is used by default
@@ -70,7 +70,7 @@ class DataStorage {
70
70
  * Returns data, stored for specific test/context (or data which was stored without test id specified).
71
71
  * This method will get data from global variable and/or from from file (previosly saved with put method).
72
72
  *
73
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
73
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
74
74
  * @param {string} context
75
75
  * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
76
76
  */
@@ -108,7 +108,7 @@ class DataStorage {
108
108
  }
109
109
 
110
110
  /**
111
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
111
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
112
112
  * @param {string} context
113
113
  * @returns aray of data (any type)
114
114
  */
@@ -127,7 +127,7 @@ class DataStorage {
127
127
  }
128
128
 
129
129
  /**
130
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
130
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
131
131
  * @param {*} context
132
132
  * @returns array of data (any type)
133
133
  */
@@ -151,7 +151,7 @@ class DataStorage {
151
151
 
152
152
  /**
153
153
  * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
154
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
154
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
155
155
  * @param {*} data
156
156
  * @param {*} context
157
157
  */
@@ -166,7 +166,7 @@ class DataStorage {
166
166
 
167
167
  /**
168
168
  * Puts data to file. Unlike the global variable storage, stores data as string
169
- * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
169
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
170
170
  * @param {*} data
171
171
  * @param {string} context
172
172
  * @returns
@@ -119,7 +119,8 @@ class TestomatioPipe {
119
119
  const resp = await this.client.request({
120
120
  method: 'GET',
121
121
  url: '/api/test_grep',
122
- ...q,
122
+ params: q.params,
123
+ responseType: q.responseType
123
124
  });
124
125
 
125
126
  if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
@@ -3,8 +3,6 @@ import { services } from './services/index.js';
3
3
  /**
4
4
  * Stores path to file as artifact and uploads it to the S3 storage
5
5
  * @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
6
- * @param {any} [context=null] - optional context parameter
7
- * @returns {void}
8
6
  */
9
7
  function saveArtifact(data, context = null) {
10
8
  if (process.env.IS_PLAYWRIGHT)
@@ -16,8 +14,7 @@ function saveArtifact(data, context = null) {
16
14
 
17
15
  /**
18
16
  * Attach log message(s) to the test report
19
- * @param {...any} args - log messages to attach
20
- * @returns {void}
17
+ * @param string
21
18
  */
22
19
  function logMessage(...args) {
23
20
  if (process.env.IS_PLAYWRIGHT) throw new Error('This function is not available in Playwright framework');
@@ -26,8 +23,7 @@ function logMessage(...args) {
26
23
 
27
24
  /**
28
25
  * Similar to "log" function but marks message in report as a step
29
- * @param {string} message - step message
30
- * @returns {void}
26
+ * @param {string} message
31
27
  */
32
28
  function addStep(message) {
33
29
  if (process.env.IS_PLAYWRIGHT)
@@ -38,9 +34,8 @@ function addStep(message) {
38
34
 
39
35
  /**
40
36
  * Add key-value pair(s) to the test report
41
- * @param {{[key: string]: string} | string} keyValue - object { key: value } (multiple props allowed) or key (string)
42
- * @param {string|null} [value=null] - optional value when keyValue is a string
43
- * @returns {void}
37
+ * @param {{[key: string]: string} | string} keyValue object { key: value } (multiple props allowed) or key (string)
38
+ * @param {string?} value
44
39
  */
45
40
  function setKeyValue(keyValue, value = null) {
46
41
  if (process.env.IS_PLAYWRIGHT)
@@ -53,32 +48,48 @@ function setKeyValue(keyValue, value = null) {
53
48
  }
54
49
 
55
50
  /**
56
- * Add label(s) to the test report
51
+ * Add a single label to the test report
57
52
  * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
58
- * @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
59
- * @returns {void}
53
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
60
54
  */
61
55
  function setLabel(key, value = null) {
62
56
  if (Array.isArray(value)) {
63
- value.forEach(val => setLabel(key, val));
57
+ value.forEach(v => setLabel(key, v));
64
58
  return;
65
59
  }
66
60
 
67
- const labelObject = value !== null && value !== undefined && value !== ''
68
- ? { label: `${key}:${value}` }
69
- : { label: key };
70
- services.links.put([labelObject]);
71
- }
61
+ if (!key || typeof key !== 'string') {
62
+ console.warn('Label key must be a non-empty string');
63
+ return;
64
+ }
72
65
 
66
+ // Limit key length to 255 characters
67
+ if (key.length > 255) {
68
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
69
+ key = key.substring(0, 255);
70
+ }
73
71
 
74
- /**
75
- * Add link(s) to the test report
76
- * @param {...string} testIds - test IDs to link
77
- * @returns {void}
78
- */
79
- function linkTest(...testIds) {
80
- const links = testIds.map(testId => ({ test: testId }));
81
- services.links.put(links);
72
+ let labelString = key;
73
+ if (value !== null && value !== undefined && value !== '') {
74
+ if (typeof value !== 'string') {
75
+ console.warn('Label value must be a string, converting:', value);
76
+ value = String(value);
77
+ }
78
+ // Limit value length to 255 characters
79
+ if (value.length > 255) {
80
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
81
+ value = value.substring(0, 255);
82
+ }
83
+ labelString = `${key}:${value}`;
84
+ }
85
+
86
+ // Limit total label length to 255 characters
87
+ if (labelString.length > 255) {
88
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
89
+ labelString = labelString.substring(0, 255);
90
+ }
91
+
92
+ services.labels.put([labelString]);
82
93
  }
83
94
 
84
95
  export default {
@@ -87,5 +98,4 @@ export default {
87
98
  step: addStep,
88
99
  keyValue: setKeyValue,
89
100
  label: setLabel,
90
- linkTest,
91
101
  };
package/src/reporter.js CHANGED
@@ -9,15 +9,14 @@ export const logger = services.logger;
9
9
  export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
11
  export const label = reporterFunctions.label;
12
- export const linkTest = reporterFunctions.linkTest;
13
12
 
14
13
  /**
15
- * @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
16
- * @typedef {typeof import('./reporter-functions.js').default.log} LogFunction
17
- * @typedef {typeof import('./services/index.js').services.logger} LoggerService
18
- * @typedef {typeof import('./reporter-functions.js').default.keyValue} MetaFunction
19
- * @typedef {typeof import('./reporter-functions.js').default.step} StepFunction
20
- * @typedef {typeof import('./reporter-functions.js').default.label} LabelFunction
14
+ * @typedef {import('./reporter-functions.js')} artifact
15
+ * @typedef {import('./reporter-functions.js')} log
16
+ * @typedef {import('./services/index.js')} logger
17
+ * @typedef {import('./reporter-functions.js')} meta
18
+ * @typedef {import('./reporter-functions.js')} step
19
+ * @typedef {import('./reporter-functions.js')} label
21
20
  */
22
21
  export default {
23
22
  /**
@@ -31,7 +30,6 @@ export default {
31
30
  meta: reporterFunctions.keyValue,
32
31
  step: reporterFunctions.step,
33
32
  label: reporterFunctions.label,
34
- linkTest: reporterFunctions.linkTest,
35
33
 
36
34
  // TestomatClient,
37
35
  // TRConstants,
@@ -1,14 +1,14 @@
1
1
  import { logger } from './logger.js';
2
2
  import { artifactStorage } from './artifacts.js';
3
3
  import { keyValueStorage } from './key-values.js';
4
- import { linkStorage } from './links.js';
4
+ import { labelStorage } from './labels.js';
5
5
  import { dataStorage } from '../data-storage.js';
6
6
 
7
7
  export const services = {
8
8
  logger,
9
9
  artifacts: artifactStorage,
10
10
  keyValues: keyValueStorage,
11
- links: linkStorage,
11
+ labels: labelStorage,
12
12
  setContext: context => {
13
13
  dataStorage.setContext(context);
14
14
  },
@@ -23,7 +23,7 @@ class LabelStorage {
23
23
  */
24
24
  put(labels, context = null) {
25
25
  if (!labels || !Array.isArray(labels)) return;
26
- dataStorage.putData('links', labels, context);
26
+ dataStorage.putData('labels', labels, context);
27
27
  }
28
28
 
29
29
  /**
@@ -32,7 +32,7 @@ class LabelStorage {
32
32
  * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
33
33
  */
34
34
  get(context = null) {
35
- const labelsList = dataStorage.getData('links', context);
35
+ const labelsList = dataStorage.getData('labels', context);
36
36
  if (!labelsList || !labelsList?.length) return [];
37
37
 
38
38
  const allLabels = [];
@@ -53,7 +53,7 @@ const parseSuite = suiteTitle => {
53
53
  */
54
54
  const validateSuiteId = suiteId => {
55
55
  if (!suiteId) return null;
56
-
56
+
57
57
  const match = suiteId.match(SUITE_ID_REGEX);
58
58
  return match ? match[0] : null;
59
59
  };
@@ -273,7 +273,7 @@ const fileSystem = {
273
273
  const foundedTestLog = (app, tests) => {
274
274
  const n = tests.length;
275
275
 
276
- return console.log(app, `✅ We found ${n === 1 ? 'one test' : `${n} tests`} in Testomat.io!`);
276
+ return n === 1 ? console.log(app, `✅ We found one test!`) : console.log(app, `✅ We found ${n} tests!`);
277
277
  };
278
278
 
279
279
  const humanize = text => {
@@ -354,14 +354,12 @@ function storeRunId(runId) {
354
354
  }
355
355
 
356
356
  /**
357
- *
357
+ *
358
358
  * @returns {String|null} latest run ID
359
359
  */
360
360
  function readLatestRunId() {
361
361
  try {
362
362
  const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
363
- if (!fs.existsSync(filePath)) return null;
364
-
365
363
  const stats = fs.statSync(filePath);
366
364
  const diff = +new Date() - +stats.mtime;
367
365
  const diffHours = diff / 1000 / 60 / 60;
package/src/xmlReader.js CHANGED
@@ -161,11 +161,8 @@ class XmlReader {
161
161
 
162
162
  reduceOptions.preferClassname = this.stats.language === 'python';
163
163
  const resultTests = processTestSuite(jsonSuite['test-suite']);
164
-
165
- // Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
166
- const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
167
164
 
168
- this.tests = this.tests.concat(deduplicatedTests);
165
+ this.tests = this.tests.concat(resultTests);
169
166
 
170
167
  return {
171
168
  status: result?.toLowerCase(),
@@ -174,7 +171,7 @@ class XmlReader {
174
171
  passed_count: parseInt(passed, 10),
175
172
  failed_count: parseInt(failed, 10),
176
173
  skipped_count: parseInt(inconclusive + skipped, 10),
177
- tests: deduplicatedTests,
174
+ tests: resultTests,
178
175
  };
179
176
  }
180
177
 
@@ -322,194 +319,6 @@ class XmlReader {
322
319
  };
323
320
  }
324
321
 
325
- deduplicateTestsByFQN(tests) {
326
- const fqnMap = new Map();
327
-
328
- tests.forEach(test => {
329
- const fqn = this.generateNormalizedFQN(test);
330
-
331
- if (fqnMap.has(fqn)) {
332
- const existingTest = fqnMap.get(fqn);
333
-
334
- // For parameterized tests, merge as Examples
335
- if (test.example) {
336
- // Initialize examples array if it doesn't exist
337
- if (!existingTest.examples) {
338
- existingTest.examples = [];
339
- // Add the existing test's example as the first item
340
- if (existingTest.example) {
341
- existingTest.examples.push({
342
- parameters: existingTest.example,
343
- status: existingTest.status,
344
- run_time: existingTest.run_time,
345
- message: existingTest.message,
346
- stack: existingTest.stack
347
- });
348
- }
349
- }
350
-
351
- // Add this test's execution as an example
352
- existingTest.examples.push({
353
- parameters: test.example,
354
- status: test.status,
355
- run_time: test.run_time,
356
- message: test.message,
357
- stack: test.stack
358
- });
359
-
360
- // Update the main test status to reflect the worst status
361
- if (test.status === 'failed' || existingTest.status === 'failed') {
362
- existingTest.status = 'failed';
363
- } else if (test.status === 'skipped' && existingTest.status !== 'failed') {
364
- existingTest.status = 'skipped';
365
- }
366
-
367
- // Update total run time
368
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
369
-
370
- // Merge stack traces if they're different
371
- if (test.stack && test.stack !== existingTest.stack) {
372
- existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
373
- }
374
-
375
- // Merge messages if they're different
376
- if (test.message && test.message !== existingTest.message) {
377
- existingTest.message = existingTest.message + '; ' + test.message;
378
- }
379
- } else {
380
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
381
- if (test.test_id && !existingTest.test_id) {
382
- existingTest.test_id = test.test_id;
383
- }
384
- // Keep the most complete test data
385
- if (test.stack && !existingTest.stack) {
386
- existingTest.stack = test.stack;
387
- }
388
- if (test.message && !existingTest.message) {
389
- existingTest.message = test.message;
390
- }
391
- }
392
-
393
- // Prefer Test Explorer structure (longer, more complete suite_title)
394
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
395
- existingTest.suite_title = test.suite_title;
396
- existingTest.file = this.extractCsFileFromPath(test);
397
- }
398
- } else {
399
- // Fix file path to use proper .cs file names from source paths
400
- test.file = this.extractCsFileFromPath(test);
401
- fqnMap.set(fqn, test);
402
- }
403
- });
404
-
405
- return Array.from(fqnMap.values());
406
- }
407
-
408
- generateFQN(test) {
409
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
410
- // Don't include assembly as it can vary between different test structures
411
- const namespace = this.extractNamespace(test);
412
- const className = this.extractClassName(test);
413
- const methodName = test.title;
414
-
415
- // Use the most complete namespace.class structure available
416
- if (test.suite_title && test.suite_title.includes('.')) {
417
- return `${test.suite_title}.${methodName}`;
418
- }
419
-
420
- return `${namespace}.${className}.${methodName}`;
421
- }
422
-
423
- generateNormalizedFQN(test) {
424
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
425
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
426
-
427
- const fullClassName = test.suite_title || '';
428
- const methodName = test.title;
429
-
430
- // Extract the most specific namespace.class pattern
431
- if (fullClassName.includes('.')) {
432
- const parts = fullClassName.split('.');
433
-
434
- if (parts.length >= 2) {
435
- const className = parts[parts.length - 1];
436
-
437
- // Look for common .NET namespace patterns and normalize them:
438
- // TestProject.Tests.MyClass -> Tests.MyClass
439
- // Tests.MyClass -> Tests.MyClass
440
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
441
-
442
- let normalizedNamespace = '';
443
- for (let i = parts.length - 2; i >= 0; i--) {
444
- const part = parts[i];
445
-
446
- // Build namespace from right to left, excluding project names
447
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
448
- // Found a test namespace, use it as the normalized namespace
449
- normalizedNamespace = part;
450
- break;
451
- } else if (i === parts.length - 2) {
452
- // If no test namespace found, use the immediate parent as namespace
453
- normalizedNamespace = part;
454
- }
455
- }
456
-
457
- return `${normalizedNamespace}.${className}.${methodName}`;
458
- }
459
- }
460
-
461
- // Fallback for simple class names
462
- return `${fullClassName}.${methodName}`;
463
- }
464
-
465
- extractAssemblyName(test) {
466
- // Extract assembly name from file path or use default
467
- if (test.file) {
468
- const parts = test.file.split(/[/\\]/);
469
- return parts[0] || 'DefaultAssembly';
470
- }
471
- return 'DefaultAssembly';
472
- }
473
-
474
- extractNamespace(test) {
475
- // Extract namespace from suite_title or classname
476
- if (test.suite_title && test.suite_title.includes('.')) {
477
- const parts = test.suite_title.split('.');
478
- return parts.slice(0, -1).join('.');
479
- }
480
- return test.suite_title || 'DefaultNamespace';
481
- }
482
-
483
- extractClassName(test) {
484
- // Extract class name from suite_title
485
- if (test.suite_title && test.suite_title.includes('.')) {
486
- const parts = test.suite_title.split('.');
487
- return parts[parts.length - 1];
488
- }
489
- return test.suite_title || 'DefaultClass';
490
- }
491
-
492
- extractCsFileFromPath(test) {
493
- // Extract .cs file name from source file path, not namespace
494
- if (test.file) {
495
- // Look for actual .cs file path patterns
496
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
497
- if (csFileMatch) {
498
- return test.file;
499
- }
500
-
501
- // If no .cs extension, assume it's a namespace path and convert to likely file name
502
- const className = this.extractClassName(test);
503
- const pathParts = test.file.split(/[/\\]/);
504
- pathParts[pathParts.length - 1] = `${className}.cs`;
505
- return pathParts.join('/');
506
- }
507
-
508
- // Fallback to class name
509
- const className = this.extractClassName(test);
510
- return `${className}.cs`;
511
- }
512
-
513
322
  calculateStats() {
514
323
  this.stats = {
515
324
  ...this.stats,
@@ -676,8 +485,7 @@ function reduceTestCases(prev, item) {
676
485
  testCases
677
486
  .filter(t => !!t)
678
487
  .forEach(testCaseItem => {
679
- // Use consistent Test Explorer structure: prioritize fullname for file path
680
- const file = extractSourceFilePath(testCaseItem, item);
488
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
681
489
 
682
490
  let stack = '';
683
491
  let message = '';
@@ -692,25 +500,19 @@ function reduceTestCases(prev, item) {
692
500
  if (!message) message = stack.trim().split('\n')[0];
693
501
 
694
502
  const isParametrized = item.type === 'ParameterizedMethod';
503
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
695
504
 
696
505
  // SpecFlow config
697
506
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
698
507
  let example = null;
699
-
700
- // Use consistent Test Explorer structure for suite title
701
- const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
508
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
702
509
 
703
510
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
704
511
  tags ||= [];
705
512
 
706
- // Store original test name for parameter extraction
707
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
708
-
709
- const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
513
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
710
514
  if (exampleMatches) {
711
- // Extract and store parameters as Examples
712
- const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
713
- example = { ...parameterValues };
515
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
714
516
  title = title.replace(/\(.*?\)/, '').trim();
715
517
  }
716
518
 
@@ -766,7 +568,6 @@ function reduceTestCases(prev, item) {
766
568
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
767
569
  status,
768
570
  title,
769
- originalTestName, // Store original name for parameter-aware FQN generation
770
571
  root_suite_id: TESTOMATIO_SUITE,
771
572
  suite_title: suiteTitle,
772
573
  files,
@@ -776,57 +577,6 @@ function reduceTestCases(prev, item) {
776
577
  return prev;
777
578
  }
778
579
 
779
- function extractSourceFilePath(testCaseItem, item) {
780
- // Priority order for file path extraction to match Test Explorer structure:
781
- // 1. fullname (contains full project path)
782
- // 2. filepath (direct file path)
783
- // 3. file attribute from test case
784
- // 4. package (fallback)
785
-
786
- if (item.fullname) {
787
- // Extract actual file path from fullname if it contains path separators
788
- const fullnameParts = item.fullname.split('.');
789
- if (fullnameParts.length > 2) {
790
- // Reconstruct path from project.namespace.class structure
791
- const projectName = fullnameParts[0];
792
- const namespaceParts = fullnameParts.slice(1, -1);
793
- const className = fullnameParts[fullnameParts.length - 1];
794
- return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
795
- }
796
- }
797
-
798
- if (item.filepath) return item.filepath;
799
- if (testCaseItem.file) return testCaseItem.file;
800
- if (item.package) return item.package;
801
-
802
- // Fallback: construct from classname
803
- if (testCaseItem.classname) {
804
- const parts = testCaseItem.classname.split('.');
805
- const className = parts[parts.length - 1];
806
- const namespacePath = parts.slice(0, -1).join('/');
807
- return `${namespacePath}/${className}.cs`;
808
- }
809
-
810
- return '';
811
- }
812
-
813
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
814
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
815
- // Priority: fullname > classname > name
816
-
817
- if (item.fullname) {
818
- // Use fullname to maintain Test Explorer structure
819
- return item.fullname;
820
- }
821
-
822
- if (testCaseItem.classname) {
823
- return testCaseItem.classname;
824
- }
825
-
826
- // Fallback to item name but prefer classname structure
827
- return item.name || testCaseItem.classname || 'UnknownClass';
828
- }
829
-
830
580
  function processTestSuite(testsuite) {
831
581
  if (!testsuite) return [];
832
582
  if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
@@ -837,17 +587,9 @@ function processTestSuite(testsuite) {
837
587
  suites = [testsuite];
838
588
  }
839
589
 
840
- // Only process suites that have test cases OR child suites, but avoid double processing
841
- const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
842
- const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
843
-
844
- // Process child suites recursively
845
- const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
846
-
847
- // Process leaf suites with actual test cases
848
- const leafResults = leafSuites.reduce(reduceTestCases, []);
590
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
849
591
 
850
- return [...childResults, ...leafResults];
592
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
851
593
  }
852
594
 
853
595
  function fetchProperties(item) {