@testomatio/reporter 2.1.3-beta.1-xml-import → 2.1.3-beta.1-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 +5 -6
- 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 +65552 -0
- package/lib/utils/utils.js +1 -3
- package/lib/xmlReader.d.ts +0 -7
- package/lib/xmlReader.js +7 -180
- package/package.json +1 -1
- package/src/adapter/codecept.js +5 -6
- 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 +7 -210
- package/src/services/links.js +0 -69
|
@@ -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,145 +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
|
-
// 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
|
-
|
|
464
322
|
calculateStats() {
|
|
465
323
|
this.stats = {
|
|
466
324
|
...this.stats,
|
|
@@ -627,8 +485,7 @@ function reduceTestCases(prev, item) {
|
|
|
627
485
|
testCases
|
|
628
486
|
.filter(t => !!t)
|
|
629
487
|
.forEach(testCaseItem => {
|
|
630
|
-
|
|
631
|
-
const file = extractSourceFilePath(testCaseItem, item);
|
|
488
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
632
489
|
|
|
633
490
|
let stack = '';
|
|
634
491
|
let message = '';
|
|
@@ -643,13 +500,12 @@ function reduceTestCases(prev, item) {
|
|
|
643
500
|
if (!message) message = stack.trim().split('\n')[0];
|
|
644
501
|
|
|
645
502
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
503
|
+
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
646
504
|
|
|
647
505
|
// SpecFlow config
|
|
648
506
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
649
507
|
let example = null;
|
|
650
|
-
|
|
651
|
-
// Use consistent Test Explorer structure for suite title
|
|
652
|
-
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
508
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
653
509
|
|
|
654
510
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
655
511
|
tags ||= [];
|
|
@@ -721,57 +577,6 @@ function reduceTestCases(prev, item) {
|
|
|
721
577
|
return prev;
|
|
722
578
|
}
|
|
723
579
|
|
|
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
|
-
|
|
775
580
|
function processTestSuite(testsuite) {
|
|
776
581
|
if (!testsuite) return [];
|
|
777
582
|
if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
|
|
@@ -782,17 +587,9 @@ function processTestSuite(testsuite) {
|
|
|
782
587
|
suites = [testsuite];
|
|
783
588
|
}
|
|
784
589
|
|
|
785
|
-
|
|
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, []);
|
|
590
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
794
591
|
|
|
795
|
-
return [...
|
|
592
|
+
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
796
593
|
}
|
|
797
594
|
|
|
798
595
|
function fetchProperties(item) {
|
package/src/services/links.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
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();
|