@testomatio/reporter 2.1.1 → 2.1.3-beta.1-xml-import
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 +2 -2
- package/lib/adapter/mocha.js +14 -0
- package/lib/adapter/webdriver.js +6 -4
- package/lib/bin/startTest.js +38 -91
- package/lib/client.js +6 -3
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/reporter-functions.d.ts +10 -3
- package/lib/reporter-functions.js +18 -28
- package/lib/reporter.d.ts +16 -14
- package/lib/reporter.js +3 -1
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -2
- package/lib/services/index.js +2 -2
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +1 -1
- package/lib/services/labels.js +2 -2
- package/lib/services/links.d.ts +22 -0
- package/lib/services/links.js +71 -0
- package/lib/services/logger.d.ts +1 -1
- package/lib/xmlReader.d.ts +7 -0
- package/lib/xmlReader.js +180 -7
- package/package.json +1 -1
- package/src/adapter/codecept.js +2 -2
- package/src/adapter/mocha.js +15 -0
- package/src/adapter/webdriver.js +6 -4
- package/src/bin/startTest.js +43 -114
- package/src/client.js +5 -3
- package/src/data-storage.js +6 -6
- package/src/reporter-functions.js +18 -29
- package/src/reporter.js +2 -0
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/services/links.js +69 -0
- package/src/xmlReader.js +210 -7
package/src/data-storage.js
CHANGED
|
@@ -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' | '
|
|
44
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} 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' | '
|
|
73
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} 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' | '
|
|
111
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} 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' | '
|
|
130
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} 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' | '
|
|
154
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} 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' | '
|
|
169
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
|
|
170
170
|
* @param {*} data
|
|
171
171
|
* @param {string} context
|
|
172
172
|
* @returns
|
|
@@ -53,44 +53,32 @@ function setKeyValue(keyValue, value = null) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Add
|
|
56
|
+
* Add label(s) to the test report
|
|
57
57
|
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
58
|
-
* @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
|
|
58
|
+
* @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
|
|
59
59
|
* @returns {void}
|
|
60
60
|
*/
|
|
61
61
|
function setLabel(key, value = null) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
value.forEach(val => setLabel(key, val));
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let labelString = key;
|
|
74
|
-
if (value !== null && value !== undefined && value !== '') {
|
|
75
|
-
if (typeof value !== 'string') {
|
|
76
|
-
console.warn('Label value must be a string, converting:', value);
|
|
77
|
-
value = String(value);
|
|
78
|
-
}
|
|
79
|
-
// Limit value length to 255 characters
|
|
80
|
-
if (value.length > 255) {
|
|
81
|
-
console.warn('Label value is too long, trimmed to 255 characters:', value);
|
|
82
|
-
value = value.substring(0, 255);
|
|
83
|
-
}
|
|
84
|
-
labelString = `${key}:${value}`;
|
|
85
|
-
}
|
|
67
|
+
const labelObject = value !== null && value !== undefined && value !== ''
|
|
68
|
+
? { label: `${key}:${value}` }
|
|
69
|
+
: { label: key };
|
|
70
|
+
services.links.put([labelObject]);
|
|
71
|
+
}
|
|
86
72
|
|
|
87
|
-
// Limit total label length to 255 characters
|
|
88
|
-
if (labelString.length > 255) {
|
|
89
|
-
console.warn('Label is too long, trimmed to 255 characters:', labelString);
|
|
90
|
-
labelString = labelString.substring(0, 255);
|
|
91
|
-
}
|
|
92
73
|
|
|
93
|
-
|
|
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);
|
|
94
82
|
}
|
|
95
83
|
|
|
96
84
|
export default {
|
|
@@ -99,4 +87,5 @@ export default {
|
|
|
99
87
|
step: addStep,
|
|
100
88
|
keyValue: setKeyValue,
|
|
101
89
|
label: setLabel,
|
|
90
|
+
linkTest,
|
|
102
91
|
};
|
package/src/reporter.js
CHANGED
|
@@ -9,6 +9,7 @@ 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;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
|
|
@@ -30,6 +31,7 @@ export default {
|
|
|
30
31
|
meta: reporterFunctions.keyValue,
|
|
31
32
|
step: reporterFunctions.step,
|
|
32
33
|
label: reporterFunctions.label,
|
|
34
|
+
linkTest: reporterFunctions.linkTest,
|
|
33
35
|
|
|
34
36
|
// TestomatClient,
|
|
35
37
|
// TRConstants,
|
package/src/services/index.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { linkStorage } from './links.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
|
-
|
|
11
|
+
links: linkStorage,
|
|
12
12
|
setContext: context => {
|
|
13
13
|
dataStorage.setContext(context);
|
|
14
14
|
},
|
package/src/services/labels.js
CHANGED
|
@@ -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('
|
|
26
|
+
dataStorage.putData('links', 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('
|
|
35
|
+
const labelsList = dataStorage.getData('links', context);
|
|
36
36
|
if (!labelsList || !labelsList?.length) return [];
|
|
37
37
|
|
|
38
38
|
const allLabels = [];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
|
+
import { dataStorage } from '../data-storage.js';
|
|
3
|
+
|
|
4
|
+
const debug = createDebugMessages('@testomatio/reporter:services-links');
|
|
5
|
+
|
|
6
|
+
class LinkStorage {
|
|
7
|
+
static #instance;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @returns {LinkStorage}
|
|
12
|
+
*/
|
|
13
|
+
static getInstance() {
|
|
14
|
+
if (!this.#instance) {
|
|
15
|
+
this.#instance = new LinkStorage();
|
|
16
|
+
}
|
|
17
|
+
return this.#instance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stores links array and passes it to reporter
|
|
22
|
+
* @param {object[]} links - array of link objects
|
|
23
|
+
* @param {*} context - full test title
|
|
24
|
+
*/
|
|
25
|
+
put(links, context = null) {
|
|
26
|
+
if (!links || !Array.isArray(links)) return;
|
|
27
|
+
dataStorage.putData('links', links, context);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns links array for the test
|
|
32
|
+
* @param {*} context testId or test context from test runner
|
|
33
|
+
* @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
|
|
34
|
+
*/
|
|
35
|
+
get(context = null) {
|
|
36
|
+
const linksList = dataStorage.getData('links', context);
|
|
37
|
+
if (!linksList || !linksList?.length) return [];
|
|
38
|
+
|
|
39
|
+
const allLinks = [];
|
|
40
|
+
for (const links of linksList) {
|
|
41
|
+
if (Array.isArray(links)) {
|
|
42
|
+
allLinks.push(...links);
|
|
43
|
+
} else if (typeof links === 'string') {
|
|
44
|
+
try {
|
|
45
|
+
const parsedLinks = JSON.parse(links);
|
|
46
|
+
if (Array.isArray(parsedLinks)) {
|
|
47
|
+
allLinks.push(...parsedLinks);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
debug(`Error parsing links for test ${context}`, links);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Remove duplicates based on JSON string comparison
|
|
56
|
+
const uniqueLinks = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const link of allLinks) {
|
|
59
|
+
const key = JSON.stringify(link);
|
|
60
|
+
if (!seen.has(key)) {
|
|
61
|
+
seen.add(key);
|
|
62
|
+
uniqueLinks.push(link);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return uniqueLinks;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const linkStorage = LinkStorage.getInstance();
|
package/src/xmlReader.js
CHANGED
|
@@ -161,8 +161,11 @@ 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);
|
|
164
167
|
|
|
165
|
-
this.tests = this.tests.concat(
|
|
168
|
+
this.tests = this.tests.concat(deduplicatedTests);
|
|
166
169
|
|
|
167
170
|
return {
|
|
168
171
|
status: result?.toLowerCase(),
|
|
@@ -171,7 +174,7 @@ class XmlReader {
|
|
|
171
174
|
passed_count: parseInt(passed, 10),
|
|
172
175
|
failed_count: parseInt(failed, 10),
|
|
173
176
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
174
|
-
tests:
|
|
177
|
+
tests: deduplicatedTests,
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -319,6 +322,145 @@ class XmlReader {
|
|
|
319
322
|
};
|
|
320
323
|
}
|
|
321
324
|
|
|
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
|
+
// Merge test properties, prioritizing Test Explorer structure but updating with IDs
|
|
334
|
+
if (test.test_id && !existingTest.test_id) {
|
|
335
|
+
existingTest.test_id = test.test_id;
|
|
336
|
+
}
|
|
337
|
+
// Keep the most complete test data
|
|
338
|
+
if (test.stack && !existingTest.stack) {
|
|
339
|
+
existingTest.stack = test.stack;
|
|
340
|
+
}
|
|
341
|
+
if (test.message && !existingTest.message) {
|
|
342
|
+
existingTest.message = test.message;
|
|
343
|
+
}
|
|
344
|
+
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
345
|
+
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
346
|
+
existingTest.suite_title = test.suite_title;
|
|
347
|
+
existingTest.file = this.extractCsFileFromPath(test);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// Fix file path to use proper .cs file names from source paths
|
|
351
|
+
test.file = this.extractCsFileFromPath(test);
|
|
352
|
+
fqnMap.set(fqn, test);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return Array.from(fqnMap.values());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
generateFQN(test) {
|
|
360
|
+
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
361
|
+
// Don't include assembly as it can vary between different test structures
|
|
362
|
+
const namespace = this.extractNamespace(test);
|
|
363
|
+
const className = this.extractClassName(test);
|
|
364
|
+
const methodName = test.title;
|
|
365
|
+
|
|
366
|
+
// Use the most complete namespace.class structure available
|
|
367
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
368
|
+
return `${test.suite_title}.${methodName}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return `${namespace}.${className}.${methodName}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
generateNormalizedFQN(test) {
|
|
375
|
+
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
376
|
+
// This normalizes different representations of the same test
|
|
377
|
+
|
|
378
|
+
const fullClassName = test.suite_title || '';
|
|
379
|
+
const methodName = test.title;
|
|
380
|
+
|
|
381
|
+
// Extract the most specific namespace.class pattern
|
|
382
|
+
if (fullClassName.includes('.')) {
|
|
383
|
+
const parts = fullClassName.split('.');
|
|
384
|
+
|
|
385
|
+
if (parts.length >= 2) {
|
|
386
|
+
const className = parts[parts.length - 1];
|
|
387
|
+
|
|
388
|
+
// Look for common .NET namespace patterns and normalize them:
|
|
389
|
+
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
390
|
+
// Tests.MyClass -> Tests.MyClass
|
|
391
|
+
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
392
|
+
|
|
393
|
+
let normalizedNamespace = '';
|
|
394
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
395
|
+
const part = parts[i];
|
|
396
|
+
|
|
397
|
+
// Build namespace from right to left, excluding project names
|
|
398
|
+
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
399
|
+
// Found a test namespace, use it as the normalized namespace
|
|
400
|
+
normalizedNamespace = part;
|
|
401
|
+
break;
|
|
402
|
+
} else if (i === parts.length - 2) {
|
|
403
|
+
// If no test namespace found, use the immediate parent as namespace
|
|
404
|
+
normalizedNamespace = part;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Fallback for simple class names
|
|
413
|
+
return `${fullClassName}.${methodName}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
extractAssemblyName(test) {
|
|
417
|
+
// Extract assembly name from file path or use default
|
|
418
|
+
if (test.file) {
|
|
419
|
+
const parts = test.file.split(/[/\\]/);
|
|
420
|
+
return parts[0] || 'DefaultAssembly';
|
|
421
|
+
}
|
|
422
|
+
return 'DefaultAssembly';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
extractNamespace(test) {
|
|
426
|
+
// Extract namespace from suite_title or classname
|
|
427
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
428
|
+
const parts = test.suite_title.split('.');
|
|
429
|
+
return parts.slice(0, -1).join('.');
|
|
430
|
+
}
|
|
431
|
+
return test.suite_title || 'DefaultNamespace';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
extractClassName(test) {
|
|
435
|
+
// Extract class name from suite_title
|
|
436
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
437
|
+
const parts = test.suite_title.split('.');
|
|
438
|
+
return parts[parts.length - 1];
|
|
439
|
+
}
|
|
440
|
+
return test.suite_title || 'DefaultClass';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
extractCsFileFromPath(test) {
|
|
444
|
+
// Extract .cs file name from source file path, not namespace
|
|
445
|
+
if (test.file) {
|
|
446
|
+
// Look for actual .cs file path patterns
|
|
447
|
+
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
448
|
+
if (csFileMatch) {
|
|
449
|
+
return test.file;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
453
|
+
const className = this.extractClassName(test);
|
|
454
|
+
const pathParts = test.file.split(/[/\\]/);
|
|
455
|
+
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
456
|
+
return pathParts.join('/');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Fallback to class name
|
|
460
|
+
const className = this.extractClassName(test);
|
|
461
|
+
return `${className}.cs`;
|
|
462
|
+
}
|
|
463
|
+
|
|
322
464
|
calculateStats() {
|
|
323
465
|
this.stats = {
|
|
324
466
|
...this.stats,
|
|
@@ -485,7 +627,8 @@ function reduceTestCases(prev, item) {
|
|
|
485
627
|
testCases
|
|
486
628
|
.filter(t => !!t)
|
|
487
629
|
.forEach(testCaseItem => {
|
|
488
|
-
|
|
630
|
+
// Use consistent Test Explorer structure: prioritize fullname for file path
|
|
631
|
+
const file = extractSourceFilePath(testCaseItem, item);
|
|
489
632
|
|
|
490
633
|
let stack = '';
|
|
491
634
|
let message = '';
|
|
@@ -500,12 +643,13 @@ function reduceTestCases(prev, item) {
|
|
|
500
643
|
if (!message) message = stack.trim().split('\n')[0];
|
|
501
644
|
|
|
502
645
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
503
|
-
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
504
646
|
|
|
505
647
|
// SpecFlow config
|
|
506
648
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
507
649
|
let example = null;
|
|
508
|
-
|
|
650
|
+
|
|
651
|
+
// Use consistent Test Explorer structure for suite title
|
|
652
|
+
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
509
653
|
|
|
510
654
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
511
655
|
tags ||= [];
|
|
@@ -577,6 +721,57 @@ function reduceTestCases(prev, item) {
|
|
|
577
721
|
return prev;
|
|
578
722
|
}
|
|
579
723
|
|
|
724
|
+
function extractSourceFilePath(testCaseItem, item) {
|
|
725
|
+
// Priority order for file path extraction to match Test Explorer structure:
|
|
726
|
+
// 1. fullname (contains full project path)
|
|
727
|
+
// 2. filepath (direct file path)
|
|
728
|
+
// 3. file attribute from test case
|
|
729
|
+
// 4. package (fallback)
|
|
730
|
+
|
|
731
|
+
if (item.fullname) {
|
|
732
|
+
// Extract actual file path from fullname if it contains path separators
|
|
733
|
+
const fullnameParts = item.fullname.split('.');
|
|
734
|
+
if (fullnameParts.length > 2) {
|
|
735
|
+
// Reconstruct path from project.namespace.class structure
|
|
736
|
+
const projectName = fullnameParts[0];
|
|
737
|
+
const namespaceParts = fullnameParts.slice(1, -1);
|
|
738
|
+
const className = fullnameParts[fullnameParts.length - 1];
|
|
739
|
+
return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (item.filepath) return item.filepath;
|
|
744
|
+
if (testCaseItem.file) return testCaseItem.file;
|
|
745
|
+
if (item.package) return item.package;
|
|
746
|
+
|
|
747
|
+
// Fallback: construct from classname
|
|
748
|
+
if (testCaseItem.classname) {
|
|
749
|
+
const parts = testCaseItem.classname.split('.');
|
|
750
|
+
const className = parts[parts.length - 1];
|
|
751
|
+
const namespacePath = parts.slice(0, -1).join('/');
|
|
752
|
+
return `${namespacePath}/${className}.cs`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return '';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
759
|
+
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
760
|
+
// Priority: fullname > classname > name
|
|
761
|
+
|
|
762
|
+
if (item.fullname) {
|
|
763
|
+
// Use fullname to maintain Test Explorer structure
|
|
764
|
+
return item.fullname;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (testCaseItem.classname) {
|
|
768
|
+
return testCaseItem.classname;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Fallback to item name but prefer classname structure
|
|
772
|
+
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
773
|
+
}
|
|
774
|
+
|
|
580
775
|
function processTestSuite(testsuite) {
|
|
581
776
|
if (!testsuite) return [];
|
|
582
777
|
if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
|
|
@@ -587,9 +782,17 @@ function processTestSuite(testsuite) {
|
|
|
587
782
|
suites = [testsuite];
|
|
588
783
|
}
|
|
589
784
|
|
|
590
|
-
|
|
785
|
+
// Only process suites that have test cases OR child suites, but avoid double processing
|
|
786
|
+
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
787
|
+
const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
|
|
788
|
+
|
|
789
|
+
// Process child suites recursively
|
|
790
|
+
const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
|
|
791
|
+
|
|
792
|
+
// Process leaf suites with actual test cases
|
|
793
|
+
const leafResults = leafSuites.reduce(reduceTestCases, []);
|
|
591
794
|
|
|
592
|
-
return [...
|
|
795
|
+
return [...childResults, ...leafResults];
|
|
593
796
|
}
|
|
594
797
|
|
|
595
798
|
function fetchProperties(item) {
|