@testomatio/reporter 2.1.3-beta.1-multi-links → 2.1.3-beta.2-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 +6 -5
- 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/pipe/testomatio.js +1 -2
- package/lib/reporter-functions.d.ts +20 -7
- package/lib/reporter-functions.js +27 -35
- package/lib/reporter.d.ts +22 -20
- package/lib/reporter.js +9 -7
- 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/utils.js +3 -1
- package/lib/xmlReader.d.ts +7 -0
- package/lib/xmlReader.js +231 -9
- package/package.json +1 -1
- package/src/adapter/codecept.js +6 -5
- 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/pipe/testomatio.js +1 -2
- package/src/reporter-functions.js +27 -37
- package/src/reporter.js +8 -6
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/services/links.js +69 -0
- package/src/utils/utils.js +5 -3
- package/src/xmlReader.js +267 -9
- package/lib/utils/cli_utils.d.ts +0 -1
- package/lib/utils/cli_utils.js +0 -65552
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,194 @@ 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
|
+
|
|
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
|
+
|
|
322
513
|
calculateStats() {
|
|
323
514
|
this.stats = {
|
|
324
515
|
...this.stats,
|
|
@@ -485,7 +676,8 @@ function reduceTestCases(prev, item) {
|
|
|
485
676
|
testCases
|
|
486
677
|
.filter(t => !!t)
|
|
487
678
|
.forEach(testCaseItem => {
|
|
488
|
-
|
|
679
|
+
// Use consistent Test Explorer structure: prioritize fullname for file path
|
|
680
|
+
const file = extractSourceFilePath(testCaseItem, item);
|
|
489
681
|
|
|
490
682
|
let stack = '';
|
|
491
683
|
let message = '';
|
|
@@ -500,19 +692,25 @@ function reduceTestCases(prev, item) {
|
|
|
500
692
|
if (!message) message = stack.trim().split('\n')[0];
|
|
501
693
|
|
|
502
694
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
503
|
-
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
504
695
|
|
|
505
696
|
// SpecFlow config
|
|
506
697
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
507
698
|
let example = null;
|
|
508
|
-
|
|
699
|
+
|
|
700
|
+
// Use consistent Test Explorer structure for suite title
|
|
701
|
+
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
509
702
|
|
|
510
703
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
511
704
|
tags ||= [];
|
|
512
705
|
|
|
513
|
-
|
|
706
|
+
// Store original test name for parameter extraction
|
|
707
|
+
const originalTestName = testCaseItem.name || testCaseItem.methodname;
|
|
708
|
+
|
|
709
|
+
const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
|
|
514
710
|
if (exampleMatches) {
|
|
515
|
-
|
|
711
|
+
// Extract and store parameters as Examples
|
|
712
|
+
const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
713
|
+
example = { ...parameterValues };
|
|
516
714
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
517
715
|
}
|
|
518
716
|
|
|
@@ -568,6 +766,7 @@ function reduceTestCases(prev, item) {
|
|
|
568
766
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
569
767
|
status,
|
|
570
768
|
title,
|
|
769
|
+
originalTestName, // Store original name for parameter-aware FQN generation
|
|
571
770
|
root_suite_id: TESTOMATIO_SUITE,
|
|
572
771
|
suite_title: suiteTitle,
|
|
573
772
|
files,
|
|
@@ -577,6 +776,57 @@ function reduceTestCases(prev, item) {
|
|
|
577
776
|
return prev;
|
|
578
777
|
}
|
|
579
778
|
|
|
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
|
+
|
|
580
830
|
function processTestSuite(testsuite) {
|
|
581
831
|
if (!testsuite) return [];
|
|
582
832
|
if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
|
|
@@ -587,9 +837,17 @@ function processTestSuite(testsuite) {
|
|
|
587
837
|
suites = [testsuite];
|
|
588
838
|
}
|
|
589
839
|
|
|
590
|
-
|
|
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, []);
|
|
591
849
|
|
|
592
|
-
return [...
|
|
850
|
+
return [...childResults, ...leafResults];
|
|
593
851
|
}
|
|
594
852
|
|
|
595
853
|
function fetchProperties(item) {
|
package/lib/utils/cli_utils.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function checkForEnvPassedAsArguments(): void;
|