@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.
- package/lib/adapter/codecept.js +3 -3
- package/lib/adapter/mocha.js +0 -14
- package/lib/adapter/webdriver.js +4 -6
- package/lib/bin/startTest.js +91 -38
- package/lib/client.js +3 -6
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/pipe/testomatio.js +2 -1
- package/lib/reporter-functions.d.ts +7 -20
- package/lib/reporter-functions.js +35 -27
- package/lib/reporter.d.ts +20 -22
- package/lib/reporter.js +7 -9
- 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/logger.d.ts +1 -1
- package/lib/utils/cli_utils.d.ts +1 -0
- package/lib/utils/cli_utils.js +262160 -0
- package/lib/utils/utils.js +1 -3
- package/lib/xmlReader.d.ts +0 -7
- package/lib/xmlReader.js +9 -231
- package/package.json +1 -1
- package/src/adapter/codecept.js +4 -3
- package/src/adapter/mocha.js +0 -15
- package/src/adapter/webdriver.js +4 -6
- package/src/bin/startTest.js +114 -43
- package/src/client.js +3 -5
- package/src/data-storage.js +6 -6
- package/src/pipe/testomatio.js +2 -1
- package/src/reporter-functions.js +37 -27
- package/src/reporter.js +6 -8
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/utils/utils.js +3 -5
- package/src/xmlReader.js +9 -267
- package/src/services/links.js +0 -69
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' | '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' | '
|
|
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' | '
|
|
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' | '
|
|
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' | '
|
|
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' | '
|
|
169
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
170
170
|
* @param {*} data
|
|
171
171
|
* @param {string} context
|
|
172
172
|
* @returns
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
42
|
-
* @param {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
|
|
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
|
|
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(
|
|
57
|
+
value.forEach(v => setLabel(key, v));
|
|
64
58
|
return;
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 {
|
|
16
|
-
* @typedef {
|
|
17
|
-
* @typedef {
|
|
18
|
-
* @typedef {
|
|
19
|
-
* @typedef {
|
|
20
|
-
* @typedef {
|
|
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,
|
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 { 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
|
-
|
|
11
|
+
labels: labelStorage,
|
|
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('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('
|
|
35
|
+
const labelsList = dataStorage.getData('labels', context);
|
|
36
36
|
if (!labelsList || !labelsList?.length) return [];
|
|
37
37
|
|
|
38
38
|
const allLabels = [];
|
package/src/utils/utils.js
CHANGED
|
@@ -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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [...
|
|
592
|
+
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
851
593
|
}
|
|
852
594
|
|
|
853
595
|
function fetchProperties(item) {
|