@testomatio/reporter 2.3.5-beta-5-xml-import β 2.3.5-beta-6-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/README.md +1 -1
- package/lib/xmlReader.d.ts +2 -0
- package/lib/xmlReader.js +50 -6
- package/package.json +1 -1
- package/src/xmlReader.js +54 -5
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Testomat.io Reporter (this npm package) supports:
|
|
|
13
13
|
- π [Stack traces](./docs/stacktrace.md) and error messages
|
|
14
14
|
- π [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
|
|
15
15
|
- π
Realtime reports
|
|
16
|
-
- ποΈ Other test frameworks supported via [
|
|
16
|
+
- ποΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
|
|
17
17
|
- πΆββοΈ Steps _(work in progress)_
|
|
18
18
|
- π [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
|
|
19
19
|
- βοΈ Custom properties and metadata _(work in progress)_
|
package/lib/xmlReader.d.ts
CHANGED
package/lib/xmlReader.js
CHANGED
|
@@ -20,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
|
|
|
20
20
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
21
21
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
22
22
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
23
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
|
|
23
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, TESTOMATIO_DISABLE_SOURCE_CODE, } = process.env;
|
|
24
24
|
const options = {
|
|
25
25
|
ignoreDeclaration: true,
|
|
26
26
|
ignoreAttributes: false,
|
|
@@ -47,6 +47,10 @@ class XmlReader {
|
|
|
47
47
|
if (!this.adapter)
|
|
48
48
|
throw new Error('XML adapter for this format not found');
|
|
49
49
|
this.opts = opts || {};
|
|
50
|
+
// Check if source code fetching should be disabled
|
|
51
|
+
this.disableSourceCodeFetching = opts.disableSourceCodeFetching || TESTOMATIO_DISABLE_SOURCE_CODE;
|
|
52
|
+
// Control suite organization strategy: 'classname' (default) or 'fullpath'
|
|
53
|
+
this.suiteOrganization = opts.suiteOrganization || process.env.TESTOMATIO_SUITE_ORGANIZATION || 'classname';
|
|
50
54
|
this.store = {};
|
|
51
55
|
this.pipesPromise = (0, index_js_1.pipesFactory)(opts, this.store);
|
|
52
56
|
this.parser = new fast_xml_parser_1.XMLParser(options);
|
|
@@ -58,6 +62,15 @@ class XmlReader {
|
|
|
58
62
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
59
63
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
60
64
|
console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
65
|
+
if (this.disableSourceCodeFetching) {
|
|
66
|
+
console.log(constants_js_1.APP_PREFIX, 'π« Source code fetching is disabled');
|
|
67
|
+
}
|
|
68
|
+
if (this.suiteOrganization === 'fullpath') {
|
|
69
|
+
console.log(constants_js_1.APP_PREFIX, 'π Using fullpath suite organization (may create nested structure)');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(constants_js_1.APP_PREFIX, 'π Using classname suite organization (avoids duplicates)');
|
|
73
|
+
}
|
|
61
74
|
}
|
|
62
75
|
connectAdapter() {
|
|
63
76
|
if (this.opts.javaTests) {
|
|
@@ -130,6 +143,7 @@ class XmlReader {
|
|
|
130
143
|
processNUnit(jsonSuite) {
|
|
131
144
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
145
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
146
|
+
reduceOptions.suiteOrganization = this.suiteOrganization;
|
|
133
147
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
134
148
|
debug('Raw tests extracted from NUnit XML:', resultTests.length);
|
|
135
149
|
debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
|
|
@@ -477,6 +491,11 @@ class XmlReader {
|
|
|
477
491
|
return this.stats;
|
|
478
492
|
}
|
|
479
493
|
fetchSourceCode() {
|
|
494
|
+
// Skip source code fetching if disabled
|
|
495
|
+
if (this.disableSourceCodeFetching) {
|
|
496
|
+
debug('Source code fetching is disabled');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
480
499
|
this.tests.forEach(t => {
|
|
481
500
|
try {
|
|
482
501
|
const file = this.adapter.getFilePath(t);
|
|
@@ -640,11 +659,8 @@ function reduceTestCases(prev, item) {
|
|
|
640
659
|
// SpecFlow config
|
|
641
660
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
642
661
|
let example = null;
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
if (!suiteTitle && item.fullname) {
|
|
646
|
-
suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
647
|
-
}
|
|
662
|
+
// Smart suite title extraction to avoid duplicates
|
|
663
|
+
const suiteTitle = getSuiteTitle(testCaseItem, item, isParametrized, reduceOptions.suiteOrganization);
|
|
648
664
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
649
665
|
tags ||= [];
|
|
650
666
|
// Store original test name for enhanced parameter extraction
|
|
@@ -832,6 +848,34 @@ function processTestSuite(testsuite) {
|
|
|
832
848
|
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
833
849
|
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
834
850
|
}
|
|
851
|
+
function getSuiteTitle(testCaseItem, item, isParametrized, suiteOrganization = 'classname') {
|
|
852
|
+
let suiteTitle;
|
|
853
|
+
if (suiteOrganization === 'fullpath') {
|
|
854
|
+
// Use full namespace path (old behavior that creates detailed structure)
|
|
855
|
+
if (item.fullname) {
|
|
856
|
+
return item.fullname;
|
|
857
|
+
}
|
|
858
|
+
suiteTitle = testCaseItem.classname || item.name;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// Use classname approach (default - avoids duplicates)
|
|
862
|
+
if (isParametrized) {
|
|
863
|
+
// For parameterized tests, use the class name to group them
|
|
864
|
+
suiteTitle = item.name || testCaseItem.classname;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
// For regular tests, prefer classname over fullname to avoid long paths
|
|
868
|
+
suiteTitle = testCaseItem.classname || item.name;
|
|
869
|
+
}
|
|
870
|
+
// If still no suite title and we have fullname, extract just the class name
|
|
871
|
+
if (!suiteTitle && item.fullname) {
|
|
872
|
+
const fullnameParts = item.fullname.split('.');
|
|
873
|
+
suiteTitle = fullnameParts[fullnameParts.length - 1]; // Just the class name
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Fallback
|
|
877
|
+
return suiteTitle || 'UnknownClass';
|
|
878
|
+
}
|
|
835
879
|
function fetchProperties(item) {
|
|
836
880
|
const tags = [];
|
|
837
881
|
let title = '';
|
package/package.json
CHANGED
package/src/xmlReader.js
CHANGED
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
TESTOMATIO_ENV,
|
|
36
36
|
TESTOMATIO_RUN,
|
|
37
37
|
TESTOMATIO_MARK_DETACHED,
|
|
38
|
+
TESTOMATIO_DISABLE_SOURCE_CODE,
|
|
38
39
|
} = process.env;
|
|
39
40
|
|
|
40
41
|
const options = {
|
|
@@ -66,6 +67,10 @@ class XmlReader {
|
|
|
66
67
|
if (!this.adapter) throw new Error('XML adapter for this format not found');
|
|
67
68
|
|
|
68
69
|
this.opts = opts || {};
|
|
70
|
+
// Check if source code fetching should be disabled
|
|
71
|
+
this.disableSourceCodeFetching = opts.disableSourceCodeFetching || TESTOMATIO_DISABLE_SOURCE_CODE;
|
|
72
|
+
// Control suite organization strategy: 'classname' (default) or 'fullpath'
|
|
73
|
+
this.suiteOrganization = opts.suiteOrganization || process.env.TESTOMATIO_SUITE_ORGANIZATION || 'classname';
|
|
69
74
|
this.store = {};
|
|
70
75
|
this.pipesPromise = pipesFactory(opts, this.store);
|
|
71
76
|
|
|
@@ -79,6 +84,16 @@ class XmlReader {
|
|
|
79
84
|
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
80
85
|
this.version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
|
|
81
86
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
87
|
+
|
|
88
|
+
if (this.disableSourceCodeFetching) {
|
|
89
|
+
console.log(APP_PREFIX, 'π« Source code fetching is disabled');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.suiteOrganization === 'fullpath') {
|
|
93
|
+
console.log(APP_PREFIX, 'π Using fullpath suite organization (may create nested structure)');
|
|
94
|
+
} else {
|
|
95
|
+
console.log(APP_PREFIX, 'π Using classname suite organization (avoids duplicates)');
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
connectAdapter() {
|
|
@@ -160,6 +175,7 @@ class XmlReader {
|
|
|
160
175
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
161
176
|
|
|
162
177
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
178
|
+
reduceOptions.suiteOrganization = this.suiteOrganization;
|
|
163
179
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
164
180
|
|
|
165
181
|
debug('Raw tests extracted from NUnit XML:', resultTests.length);
|
|
@@ -551,6 +567,12 @@ class XmlReader {
|
|
|
551
567
|
}
|
|
552
568
|
|
|
553
569
|
fetchSourceCode() {
|
|
570
|
+
// Skip source code fetching if disabled
|
|
571
|
+
if (this.disableSourceCodeFetching) {
|
|
572
|
+
debug('Source code fetching is disabled');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
554
576
|
this.tests.forEach(t => {
|
|
555
577
|
try {
|
|
556
578
|
const file = this.adapter.getFilePath(t);
|
|
@@ -728,11 +750,8 @@ function reduceTestCases(prev, item) {
|
|
|
728
750
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
729
751
|
let example = null;
|
|
730
752
|
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
if (!suiteTitle && item.fullname) {
|
|
734
|
-
suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
735
|
-
}
|
|
753
|
+
// Smart suite title extraction to avoid duplicates
|
|
754
|
+
const suiteTitle = getSuiteTitle(testCaseItem, item, isParametrized, reduceOptions.suiteOrganization);
|
|
736
755
|
|
|
737
756
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
738
757
|
tags ||= [];
|
|
@@ -942,6 +961,36 @@ function processTestSuite(testsuite) {
|
|
|
942
961
|
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
943
962
|
}
|
|
944
963
|
|
|
964
|
+
function getSuiteTitle(testCaseItem, item, isParametrized, suiteOrganization = 'classname') {
|
|
965
|
+
let suiteTitle;
|
|
966
|
+
|
|
967
|
+
if (suiteOrganization === 'fullpath') {
|
|
968
|
+
// Use full namespace path (old behavior that creates detailed structure)
|
|
969
|
+
if (item.fullname) {
|
|
970
|
+
return item.fullname;
|
|
971
|
+
}
|
|
972
|
+
suiteTitle = testCaseItem.classname || item.name;
|
|
973
|
+
} else {
|
|
974
|
+
// Use classname approach (default - avoids duplicates)
|
|
975
|
+
if (isParametrized) {
|
|
976
|
+
// For parameterized tests, use the class name to group them
|
|
977
|
+
suiteTitle = item.name || testCaseItem.classname;
|
|
978
|
+
} else {
|
|
979
|
+
// For regular tests, prefer classname over fullname to avoid long paths
|
|
980
|
+
suiteTitle = testCaseItem.classname || item.name;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// If still no suite title and we have fullname, extract just the class name
|
|
984
|
+
if (!suiteTitle && item.fullname) {
|
|
985
|
+
const fullnameParts = item.fullname.split('.');
|
|
986
|
+
suiteTitle = fullnameParts[fullnameParts.length - 1]; // Just the class name
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Fallback
|
|
991
|
+
return suiteTitle || 'UnknownClass';
|
|
992
|
+
}
|
|
993
|
+
|
|
945
994
|
function fetchProperties(item) {
|
|
946
995
|
const tags = [];
|
|
947
996
|
let title = '';
|