@elisra-devops/docgen-data-provider 1.72.0 → 1.74.0
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/bin/modules/ResultDataProvider.d.ts +38 -0
- package/bin/modules/ResultDataProvider.js +688 -7
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +438 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +850 -7
- package/src/tests/modules/ResultDataProvider.test.ts +555 -0
|
@@ -29,6 +29,17 @@ const pLimit = require('p-limit');
|
|
|
29
29
|
* Instantiate the class with the organization URL and token, and use the provided methods to fetch and process test data.
|
|
30
30
|
*/
|
|
31
31
|
export default class ResultDataProvider {
|
|
32
|
+
private static readonly MEWP_L2_COVERAGE_COLUMNS = [
|
|
33
|
+
'Customer ID',
|
|
34
|
+
'Title (Customer name)',
|
|
35
|
+
'Responsibility - SAPWBS (ESUK/IL)',
|
|
36
|
+
'Test case id',
|
|
37
|
+
'Test case title',
|
|
38
|
+
'Number of passed steps',
|
|
39
|
+
'Number of failed steps',
|
|
40
|
+
'Number of not run tests',
|
|
41
|
+
];
|
|
42
|
+
|
|
32
43
|
orgUrl: string = '';
|
|
33
44
|
token: string = '';
|
|
34
45
|
private limit = pLimit(10);
|
|
@@ -375,6 +386,127 @@ export default class ResultDataProvider {
|
|
|
375
386
|
}
|
|
376
387
|
}
|
|
377
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Builds MEWP L2 requirement coverage rows for audit reporting.
|
|
391
|
+
* Rows are one Requirement-TestCase pair; uncovered requirements are emitted with empty test-case columns.
|
|
392
|
+
*/
|
|
393
|
+
public async getMewpL2CoverageFlatResults(
|
|
394
|
+
testPlanId: string,
|
|
395
|
+
projectName: string,
|
|
396
|
+
selectedSuiteIds: number[] | undefined,
|
|
397
|
+
linkedQueryRequest?: any
|
|
398
|
+
) {
|
|
399
|
+
const defaultPayload = {
|
|
400
|
+
sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
|
|
401
|
+
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
402
|
+
rows: [] as any[],
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
407
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
408
|
+
const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
|
|
409
|
+
|
|
410
|
+
const requirements = await this.fetchMewpL2Requirements(projectName, linkedQueryRequest);
|
|
411
|
+
if (requirements.length === 0) {
|
|
412
|
+
return {
|
|
413
|
+
...defaultPayload,
|
|
414
|
+
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const requirementIndex = new Map<string, Map<number, { passed: number; failed: number; notRun: number }>>();
|
|
419
|
+
const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
|
|
420
|
+
const requirementKeys = new Set<string>();
|
|
421
|
+
requirements.forEach((requirement) => {
|
|
422
|
+
const key = this.toRequirementKey(requirement.requirementId);
|
|
423
|
+
if (!key) return;
|
|
424
|
+
requirementKeys.add(key);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
|
|
428
|
+
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
429
|
+
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
430
|
+
|
|
431
|
+
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
432
|
+
for (const runResult of runResults) {
|
|
433
|
+
const testCaseId = this.extractMewpTestCaseId(runResult);
|
|
434
|
+
const runTestCaseTitle = this.toMewpComparableText(
|
|
435
|
+
runResult?.testCase?.name || runResult?.testCaseName || runResult?.testCaseTitle
|
|
436
|
+
);
|
|
437
|
+
if (Number.isFinite(testCaseId) && testCaseId > 0 && runTestCaseTitle && !testCaseTitleMap.has(testCaseId)) {
|
|
438
|
+
testCaseTitleMap.set(testCaseId, runTestCaseTitle);
|
|
439
|
+
}
|
|
440
|
+
const actionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
441
|
+
? runResult.iteration.actionResults
|
|
442
|
+
: [];
|
|
443
|
+
const hasExecutedRun =
|
|
444
|
+
Number(runResult?.lastRunId || 0) > 0 && Number(runResult?.lastResultId || 0) > 0;
|
|
445
|
+
|
|
446
|
+
if (actionResults.length > 0) {
|
|
447
|
+
for (const actionResult of actionResults) {
|
|
448
|
+
if (actionResult?.isSharedStepTitle) continue;
|
|
449
|
+
const stepStatus = this.classifyRequirementStepOutcome(actionResult?.outcome);
|
|
450
|
+
this.accumulateRequirementCountsFromStepText(
|
|
451
|
+
`${String(actionResult?.action || '')} ${String(actionResult?.expected || '')}`,
|
|
452
|
+
stepStatus,
|
|
453
|
+
testCaseId,
|
|
454
|
+
requirementKeys,
|
|
455
|
+
requirementIndex,
|
|
456
|
+
observedTestCaseIdsByRequirement
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Do not force "not run" from definition steps when a run exists:
|
|
463
|
+
// some runs may have missing/unmapped actionResults.
|
|
464
|
+
if (hasExecutedRun) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!Number.isFinite(testCaseId)) continue;
|
|
469
|
+
if (!parsedDefinitionStepsByTestCase.has(testCaseId)) {
|
|
470
|
+
const stepsXml = testCaseStepsXmlMap.get(testCaseId) || '';
|
|
471
|
+
const parsed =
|
|
472
|
+
stepsXml && String(stepsXml).trim() !== ''
|
|
473
|
+
? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
|
|
474
|
+
: [];
|
|
475
|
+
parsedDefinitionStepsByTestCase.set(testCaseId, parsed);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
|
|
479
|
+
for (const step of definitionSteps) {
|
|
480
|
+
if (step?.isSharedStepTitle) continue;
|
|
481
|
+
this.accumulateRequirementCountsFromStepText(
|
|
482
|
+
`${String(step?.action || '')} ${String(step?.expected || '')}`,
|
|
483
|
+
'notRun',
|
|
484
|
+
testCaseId,
|
|
485
|
+
requirementKeys,
|
|
486
|
+
requirementIndex,
|
|
487
|
+
observedTestCaseIdsByRequirement
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const rows = this.buildMewpCoverageRows(
|
|
493
|
+
requirements,
|
|
494
|
+
requirementIndex,
|
|
495
|
+
observedTestCaseIdsByRequirement,
|
|
496
|
+
testCaseTitleMap
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
501
|
+
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
502
|
+
rows,
|
|
503
|
+
};
|
|
504
|
+
} catch (error: any) {
|
|
505
|
+
logger.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
|
|
506
|
+
return defaultPayload;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
378
510
|
/**
|
|
379
511
|
* Mapping each attachment to a proper URL for downloading it
|
|
380
512
|
* @param runResults Array of run results
|
|
@@ -413,6 +545,690 @@ export default class ResultDataProvider {
|
|
|
413
545
|
});
|
|
414
546
|
}
|
|
415
547
|
|
|
548
|
+
private buildMewpCoverageSheetName(planName: string, testPlanId: string): string {
|
|
549
|
+
const suffix = String(planName || '').trim() || `Plan ${testPlanId}`;
|
|
550
|
+
return `MEWP L2 Coverage - ${suffix}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private createMewpCoverageRow(
|
|
554
|
+
requirement: {
|
|
555
|
+
requirementId: string;
|
|
556
|
+
title: string;
|
|
557
|
+
responsibility: string;
|
|
558
|
+
},
|
|
559
|
+
testCaseId: number | undefined,
|
|
560
|
+
testCaseTitle: string,
|
|
561
|
+
stepSummary: { passed: number; failed: number; notRun: number }
|
|
562
|
+
) {
|
|
563
|
+
const customerId = String(requirement.requirementId || '').trim();
|
|
564
|
+
const customerTitle = String(requirement.title || '').trim();
|
|
565
|
+
const responsibility = String(requirement.responsibility || '').trim();
|
|
566
|
+
const safeTestCaseId = Number.isFinite(testCaseId) && Number(testCaseId) > 0 ? Number(testCaseId) : '';
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
'Customer ID': customerId,
|
|
570
|
+
'Title (Customer name)': customerTitle,
|
|
571
|
+
'Responsibility - SAPWBS (ESUK/IL)': responsibility,
|
|
572
|
+
'Test case id': safeTestCaseId,
|
|
573
|
+
'Test case title': String(testCaseTitle || '').trim(),
|
|
574
|
+
'Number of passed steps': Number.isFinite(stepSummary?.passed) ? stepSummary.passed : 0,
|
|
575
|
+
'Number of failed steps': Number.isFinite(stepSummary?.failed) ? stepSummary.failed : 0,
|
|
576
|
+
'Number of not run tests': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private buildMewpCoverageRows(
|
|
581
|
+
requirements: Array<{
|
|
582
|
+
requirementId: string;
|
|
583
|
+
title: string;
|
|
584
|
+
responsibility: string;
|
|
585
|
+
linkedTestCaseIds: number[];
|
|
586
|
+
}>,
|
|
587
|
+
requirementIndex: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
588
|
+
observedTestCaseIdsByRequirement: Map<string, Set<number>>,
|
|
589
|
+
testCaseTitleMap: Map<number, string>
|
|
590
|
+
): any[] {
|
|
591
|
+
const rows: any[] = [];
|
|
592
|
+
for (const requirement of requirements) {
|
|
593
|
+
const key = this.toRequirementKey(requirement.requirementId);
|
|
594
|
+
const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
|
|
595
|
+
(id) => Number.isFinite(id) && Number(id) > 0
|
|
596
|
+
);
|
|
597
|
+
const observedTestCaseIds = key
|
|
598
|
+
? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
|
|
599
|
+
: [];
|
|
600
|
+
|
|
601
|
+
const testCaseIds = Array.from(new Set<number>([...linkedTestCaseIds, ...observedTestCaseIds])).sort(
|
|
602
|
+
(a, b) => a - b
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
if (testCaseIds.length === 0) {
|
|
606
|
+
rows.push(
|
|
607
|
+
this.createMewpCoverageRow(requirement, undefined, '', {
|
|
608
|
+
passed: 0,
|
|
609
|
+
failed: 0,
|
|
610
|
+
notRun: 0,
|
|
611
|
+
})
|
|
612
|
+
);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
for (const testCaseId of testCaseIds) {
|
|
617
|
+
const summary = key
|
|
618
|
+
? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
|
|
619
|
+
: { passed: 0, failed: 0, notRun: 0 };
|
|
620
|
+
rows.push(
|
|
621
|
+
this.createMewpCoverageRow(
|
|
622
|
+
requirement,
|
|
623
|
+
testCaseId,
|
|
624
|
+
String(testCaseTitleMap.get(testCaseId) || ''),
|
|
625
|
+
summary
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return rows;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
|
|
635
|
+
const map = new Map<number, string>();
|
|
636
|
+
|
|
637
|
+
const readTitleFromWorkItemFields = (workItemFields: any): string => {
|
|
638
|
+
if (!Array.isArray(workItemFields)) return '';
|
|
639
|
+
for (const field of workItemFields) {
|
|
640
|
+
const keyCandidates = [field?.key, field?.name, field?.referenceName, field?.id]
|
|
641
|
+
.map((item) => String(item || '').toLowerCase().trim());
|
|
642
|
+
const isTitleField =
|
|
643
|
+
keyCandidates.includes('system.title') || keyCandidates.includes('title');
|
|
644
|
+
if (!isTitleField) continue;
|
|
645
|
+
const value = this.toMewpComparableText(field?.value);
|
|
646
|
+
if (value) return value;
|
|
647
|
+
}
|
|
648
|
+
return '';
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
for (const suite of testData || []) {
|
|
652
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
653
|
+
for (const point of testPointsItems) {
|
|
654
|
+
const pointTestCaseId = Number(point?.testCaseId || point?.testCase?.id);
|
|
655
|
+
if (!Number.isFinite(pointTestCaseId) || pointTestCaseId <= 0 || map.has(pointTestCaseId)) continue;
|
|
656
|
+
const pointTitle = this.toMewpComparableText(point?.testCaseName || point?.testCase?.name);
|
|
657
|
+
if (pointTitle) map.set(pointTestCaseId, pointTitle);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
661
|
+
for (const testCase of testCasesItems) {
|
|
662
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id);
|
|
663
|
+
if (!Number.isFinite(id) || id <= 0 || map.has(id)) continue;
|
|
664
|
+
const fromDirectFields = this.toMewpComparableText(
|
|
665
|
+
testCase?.testCaseName || testCase?.name || testCase?.workItem?.name
|
|
666
|
+
);
|
|
667
|
+
if (fromDirectFields) {
|
|
668
|
+
map.set(id, fromDirectFields);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const fromWorkItemField = readTitleFromWorkItemFields(testCase?.workItem?.workItemFields);
|
|
672
|
+
if (fromWorkItemField) {
|
|
673
|
+
map.set(id, fromWorkItemField);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return map;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private extractMewpTestCaseId(runResult: any): number {
|
|
682
|
+
const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
|
|
683
|
+
return Number.isFinite(testCaseId) ? testCaseId : 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private buildTestCaseStepsXmlMap(testData: any[]): Map<number, string> {
|
|
687
|
+
const map = new Map<number, string>();
|
|
688
|
+
for (const suite of testData || []) {
|
|
689
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
690
|
+
for (const testCase of testCasesItems) {
|
|
691
|
+
const id = Number(testCase?.workItem?.id);
|
|
692
|
+
if (!Number.isFinite(id)) continue;
|
|
693
|
+
if (map.has(id)) continue;
|
|
694
|
+
const fields = testCase?.workItem?.workItemFields;
|
|
695
|
+
const stepsXml = this.extractStepsXmlFromWorkItemFields(fields);
|
|
696
|
+
if (stepsXml) {
|
|
697
|
+
map.set(id, stepsXml);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return map;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private extractStepsXmlFromWorkItemFields(workItemFields: any): string {
|
|
705
|
+
if (!Array.isArray(workItemFields)) return '';
|
|
706
|
+
const isStepsKey = (name: string) => {
|
|
707
|
+
const normalized = String(name || '').toLowerCase().trim();
|
|
708
|
+
return normalized === 'steps' || normalized === 'microsoft.vsts.tcm.steps';
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
for (const field of workItemFields) {
|
|
712
|
+
const keyCandidates = [field?.key, field?.name, field?.referenceName, field?.id];
|
|
713
|
+
const hasStepsKey = keyCandidates.some((candidate) => isStepsKey(String(candidate || '')));
|
|
714
|
+
if (!hasStepsKey) continue;
|
|
715
|
+
const value = String(field?.value || '').trim();
|
|
716
|
+
if (value) return value;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return '';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private classifyRequirementStepOutcome(outcome: any): 'passed' | 'failed' | 'notRun' {
|
|
723
|
+
const normalized = String(outcome || '')
|
|
724
|
+
.trim()
|
|
725
|
+
.toLowerCase();
|
|
726
|
+
if (normalized === 'passed') return 'passed';
|
|
727
|
+
if (normalized === 'failed') return 'failed';
|
|
728
|
+
return 'notRun';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private accumulateRequirementCountsFromStepText(
|
|
732
|
+
stepText: string,
|
|
733
|
+
status: 'passed' | 'failed' | 'notRun',
|
|
734
|
+
testCaseId: number,
|
|
735
|
+
requirementKeys: Set<string>,
|
|
736
|
+
counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
737
|
+
observedTestCaseIdsByRequirement: Map<string, Set<number>>
|
|
738
|
+
) {
|
|
739
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
|
|
740
|
+
|
|
741
|
+
const codes = this.extractRequirementCodesFromText(stepText);
|
|
742
|
+
for (const code of codes) {
|
|
743
|
+
if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
|
|
744
|
+
if (!counters.has(code)) {
|
|
745
|
+
counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
|
|
746
|
+
}
|
|
747
|
+
const perTestCaseCounters = counters.get(code)!;
|
|
748
|
+
if (!perTestCaseCounters.has(testCaseId)) {
|
|
749
|
+
perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!observedTestCaseIdsByRequirement.has(code)) {
|
|
753
|
+
observedTestCaseIdsByRequirement.set(code, new Set<number>());
|
|
754
|
+
}
|
|
755
|
+
observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
|
|
756
|
+
|
|
757
|
+
const counter = perTestCaseCounters.get(testCaseId)!;
|
|
758
|
+
if (status === 'passed') counter.passed += 1;
|
|
759
|
+
else if (status === 'failed') counter.failed += 1;
|
|
760
|
+
else counter.notRun += 1;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private extractRequirementCodesFromText(text: string): Set<string> {
|
|
765
|
+
const out = new Set<string>();
|
|
766
|
+
const source = this.normalizeRequirementStepText(text);
|
|
767
|
+
// Supports SR<ID> patterns even when HTML formatting breaks the token,
|
|
768
|
+
// e.g. "S<b>R</b> 0 0 1" or "S R 0 0 1".
|
|
769
|
+
const regex = /S[\s\u00A0]*R(?:[\s\u00A0\-_]*\d){1,12}/gi;
|
|
770
|
+
let match: RegExpExecArray | null = null;
|
|
771
|
+
while ((match = regex.exec(source)) !== null) {
|
|
772
|
+
const digitsOnly = String(match[0] || '').replace(/\D/g, '');
|
|
773
|
+
const digits = Number.parseInt(digitsOnly, 10);
|
|
774
|
+
if (Number.isFinite(digits)) {
|
|
775
|
+
out.add(`SR${digits}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private normalizeRequirementStepText(text: string): string {
|
|
782
|
+
const raw = String(text || '');
|
|
783
|
+
if (!raw) return '';
|
|
784
|
+
|
|
785
|
+
return raw
|
|
786
|
+
.replace(/ | | /gi, ' ')
|
|
787
|
+
.replace(/</gi, '<')
|
|
788
|
+
.replace(/>/gi, '>')
|
|
789
|
+
.replace(/&/gi, '&')
|
|
790
|
+
.replace(/"/gi, '"')
|
|
791
|
+
.replace(/'|'/gi, "'")
|
|
792
|
+
.replace(/<[^>]*>/g, ' ')
|
|
793
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
794
|
+
.replace(/\s+/g, ' ');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private toRequirementKey(requirementId: string): string {
|
|
798
|
+
const normalized = this.normalizeMewpRequirementCode(requirementId);
|
|
799
|
+
if (!normalized) return '';
|
|
800
|
+
const digits = Number.parseInt(normalized.replace(/^SR/i, ''), 10);
|
|
801
|
+
if (!Number.isFinite(digits)) return '';
|
|
802
|
+
return `SR${digits}`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private async fetchMewpL2Requirements(projectName: string, linkedQueryRequest?: any): Promise<
|
|
806
|
+
Array<{
|
|
807
|
+
workItemId: number;
|
|
808
|
+
requirementId: string;
|
|
809
|
+
title: string;
|
|
810
|
+
responsibility: string;
|
|
811
|
+
linkedTestCaseIds: number[];
|
|
812
|
+
}>
|
|
813
|
+
> {
|
|
814
|
+
const queryHref = this.extractMewpQueryHref(linkedQueryRequest);
|
|
815
|
+
if (queryHref) {
|
|
816
|
+
return this.fetchMewpL2RequirementsFromQuery(projectName, queryHref);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
820
|
+
if (workItemTypeNames.length === 0) {
|
|
821
|
+
return [];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const quotedTypeNames = workItemTypeNames
|
|
825
|
+
.map((name) => `'${String(name).replace(/'/g, "''")}'`)
|
|
826
|
+
.join(', ');
|
|
827
|
+
const wiql = `SELECT [System.Id]
|
|
828
|
+
FROM WorkItems
|
|
829
|
+
WHERE [System.TeamProject] = @project
|
|
830
|
+
AND [System.WorkItemType] IN (${quotedTypeNames})
|
|
831
|
+
ORDER BY [System.Id]`;
|
|
832
|
+
const wiqlUrl = `${this.orgUrl}${projectName}/_apis/wit/wiql?api-version=7.1-preview.2`;
|
|
833
|
+
const wiqlResponse = await TFSServices.postRequest(wiqlUrl, this.token, 'Post', { query: wiql }, null);
|
|
834
|
+
const workItemRefs = Array.isArray(wiqlResponse?.data?.workItems) ? wiqlResponse.data.workItems : [];
|
|
835
|
+
const requirementIds = workItemRefs
|
|
836
|
+
.map((item: any) => Number(item?.id))
|
|
837
|
+
.filter((id: number) => Number.isFinite(id));
|
|
838
|
+
|
|
839
|
+
if (requirementIds.length === 0) {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
844
|
+
const requirements = workItems.map((wi: any) => {
|
|
845
|
+
const fields = wi?.fields || {};
|
|
846
|
+
return {
|
|
847
|
+
workItemId: Number(wi?.id || 0),
|
|
848
|
+
requirementId: this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0)),
|
|
849
|
+
title: String(fields['System.Title'] || ''),
|
|
850
|
+
responsibility: this.deriveMewpResponsibility(fields),
|
|
851
|
+
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
return requirements.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private extractMewpQueryHref(linkedQueryRequest?: any): string {
|
|
859
|
+
const mode = String(linkedQueryRequest?.linkedQueryMode || '')
|
|
860
|
+
.trim()
|
|
861
|
+
.toLowerCase();
|
|
862
|
+
if (mode !== 'query') return '';
|
|
863
|
+
|
|
864
|
+
return String(linkedQueryRequest?.testAssociatedQuery?.wiql?.href || '').trim();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private async fetchMewpL2RequirementsFromQuery(
|
|
868
|
+
projectName: string,
|
|
869
|
+
queryHref: string
|
|
870
|
+
): Promise<
|
|
871
|
+
Array<{
|
|
872
|
+
workItemId: number;
|
|
873
|
+
requirementId: string;
|
|
874
|
+
title: string;
|
|
875
|
+
responsibility: string;
|
|
876
|
+
linkedTestCaseIds: number[];
|
|
877
|
+
}>
|
|
878
|
+
> {
|
|
879
|
+
try {
|
|
880
|
+
const ticketsDataProvider = new TicketsDataProvider(this.orgUrl, this.token);
|
|
881
|
+
const queryResult = await ticketsDataProvider.GetQueryResultsFromWiql(
|
|
882
|
+
queryHref,
|
|
883
|
+
true,
|
|
884
|
+
new Map<number, Set<any>>()
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const requirementTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
888
|
+
const requirementTypeSet = new Set(
|
|
889
|
+
requirementTypeNames.map((name) => String(name || '').trim().toLowerCase())
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const requirementsById = new Map<
|
|
893
|
+
number,
|
|
894
|
+
{
|
|
895
|
+
workItemId: number;
|
|
896
|
+
requirementId: string;
|
|
897
|
+
title: string;
|
|
898
|
+
responsibility: string;
|
|
899
|
+
linkedTestCaseIds: Set<number>;
|
|
900
|
+
}
|
|
901
|
+
>();
|
|
902
|
+
|
|
903
|
+
const upsertRequirement = (workItem: any) => {
|
|
904
|
+
this.upsertMewpRequirement(requirementsById, workItem, requirementTypeSet);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const linkRequirementToTestCase = (requirementWorkItem: any, testCaseWorkItem: any) => {
|
|
908
|
+
const requirementId = Number(requirementWorkItem?.id || 0);
|
|
909
|
+
const testCaseId = Number(testCaseWorkItem?.id || 0);
|
|
910
|
+
if (!Number.isFinite(requirementId) || requirementId <= 0) return;
|
|
911
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
|
|
912
|
+
|
|
913
|
+
upsertRequirement(requirementWorkItem);
|
|
914
|
+
const requirement = requirementsById.get(requirementId);
|
|
915
|
+
if (!requirement) return;
|
|
916
|
+
requirement.linkedTestCaseIds.add(testCaseId);
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
if (Array.isArray(queryResult?.fetchedWorkItems)) {
|
|
920
|
+
for (const workItem of queryResult.fetchedWorkItems) {
|
|
921
|
+
upsertRequirement(workItem);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (queryResult?.sourceTargetsMap && typeof queryResult.sourceTargetsMap.entries === 'function') {
|
|
926
|
+
for (const [sourceItem, targets] of queryResult.sourceTargetsMap.entries()) {
|
|
927
|
+
const sourceType = this.getMewpWorkItemType(sourceItem);
|
|
928
|
+
const sourceIsRequirement = this.isMewpRequirementType(sourceType, requirementTypeSet);
|
|
929
|
+
const sourceIsTestCase = this.isMewpTestCaseType(sourceType);
|
|
930
|
+
|
|
931
|
+
if (sourceIsRequirement) {
|
|
932
|
+
upsertRequirement(sourceItem);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const relatedItems = Array.isArray(targets) ? targets : [];
|
|
936
|
+
for (const targetItem of relatedItems) {
|
|
937
|
+
const targetType = this.getMewpWorkItemType(targetItem);
|
|
938
|
+
const targetIsRequirement = this.isMewpRequirementType(targetType, requirementTypeSet);
|
|
939
|
+
const targetIsTestCase = this.isMewpTestCaseType(targetType);
|
|
940
|
+
|
|
941
|
+
if (targetIsRequirement) {
|
|
942
|
+
upsertRequirement(targetItem);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (sourceIsRequirement && targetIsTestCase) {
|
|
946
|
+
linkRequirementToTestCase(sourceItem, targetItem);
|
|
947
|
+
} else if (sourceIsTestCase && targetIsRequirement) {
|
|
948
|
+
linkRequirementToTestCase(targetItem, sourceItem);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
await this.hydrateMewpRequirementsFromWorkItems(projectName, requirementsById);
|
|
955
|
+
|
|
956
|
+
return [...requirementsById.values()]
|
|
957
|
+
.map((requirement) => ({
|
|
958
|
+
workItemId: requirement.workItemId,
|
|
959
|
+
requirementId: requirement.requirementId,
|
|
960
|
+
title: requirement.title,
|
|
961
|
+
responsibility: requirement.responsibility,
|
|
962
|
+
linkedTestCaseIds: [...requirement.linkedTestCaseIds].sort((a, b) => a - b),
|
|
963
|
+
}))
|
|
964
|
+
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
965
|
+
} catch (error: any) {
|
|
966
|
+
logger.error(`Could not fetch MEWP requirements from query: ${error?.message || error}`);
|
|
967
|
+
return [];
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private upsertMewpRequirement(
|
|
972
|
+
requirementsById: Map<
|
|
973
|
+
number,
|
|
974
|
+
{
|
|
975
|
+
workItemId: number;
|
|
976
|
+
requirementId: string;
|
|
977
|
+
title: string;
|
|
978
|
+
responsibility: string;
|
|
979
|
+
linkedTestCaseIds: Set<number>;
|
|
980
|
+
}
|
|
981
|
+
>,
|
|
982
|
+
workItem: any,
|
|
983
|
+
requirementTypeSet: Set<string>
|
|
984
|
+
) {
|
|
985
|
+
const workItemId = Number(workItem?.id || 0);
|
|
986
|
+
if (!Number.isFinite(workItemId) || workItemId <= 0) return;
|
|
987
|
+
|
|
988
|
+
const fields = workItem?.fields || {};
|
|
989
|
+
const workItemType = this.getMewpWorkItemType(workItem);
|
|
990
|
+
if (!this.isMewpRequirementType(workItemType, requirementTypeSet)) return;
|
|
991
|
+
|
|
992
|
+
const existing = requirementsById.get(workItemId) || {
|
|
993
|
+
workItemId,
|
|
994
|
+
requirementId: String(workItemId),
|
|
995
|
+
title: '',
|
|
996
|
+
responsibility: '',
|
|
997
|
+
linkedTestCaseIds: new Set<number>(),
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const extractedRequirementId = this.extractMewpRequirementIdentifier(fields, workItemId);
|
|
1001
|
+
const extractedTitle = this.toMewpComparableText(fields?.['System.Title']);
|
|
1002
|
+
const extractedResponsibility = this.deriveMewpResponsibility(fields);
|
|
1003
|
+
|
|
1004
|
+
existing.requirementId = extractedRequirementId || existing.requirementId || String(workItemId);
|
|
1005
|
+
if (extractedTitle) {
|
|
1006
|
+
existing.title = extractedTitle;
|
|
1007
|
+
}
|
|
1008
|
+
if (extractedResponsibility) {
|
|
1009
|
+
existing.responsibility = extractedResponsibility;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
requirementsById.set(workItemId, existing);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private async hydrateMewpRequirementsFromWorkItems(
|
|
1016
|
+
projectName: string,
|
|
1017
|
+
requirementsById: Map<
|
|
1018
|
+
number,
|
|
1019
|
+
{
|
|
1020
|
+
workItemId: number;
|
|
1021
|
+
requirementId: string;
|
|
1022
|
+
title: string;
|
|
1023
|
+
responsibility: string;
|
|
1024
|
+
linkedTestCaseIds: Set<number>;
|
|
1025
|
+
}
|
|
1026
|
+
>
|
|
1027
|
+
) {
|
|
1028
|
+
const requirementIds = [...requirementsById.keys()];
|
|
1029
|
+
if (requirementIds.length === 0) return;
|
|
1030
|
+
|
|
1031
|
+
const fetchedRequirements = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
1032
|
+
for (const requirementWorkItem of fetchedRequirements) {
|
|
1033
|
+
const workItemId = Number(requirementWorkItem?.id || 0);
|
|
1034
|
+
if (!Number.isFinite(workItemId) || workItemId <= 0) continue;
|
|
1035
|
+
const current = requirementsById.get(workItemId);
|
|
1036
|
+
if (!current) continue;
|
|
1037
|
+
|
|
1038
|
+
const fields = requirementWorkItem?.fields || {};
|
|
1039
|
+
const requirementId = this.extractMewpRequirementIdentifier(fields, workItemId);
|
|
1040
|
+
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
1041
|
+
const responsibility = this.deriveMewpResponsibility(fields);
|
|
1042
|
+
const linkedTestCaseIds = this.extractLinkedTestCaseIdsFromRequirement(
|
|
1043
|
+
requirementWorkItem?.relations || []
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
current.requirementId = requirementId || current.requirementId || String(workItemId);
|
|
1047
|
+
if (title) {
|
|
1048
|
+
current.title = title;
|
|
1049
|
+
}
|
|
1050
|
+
if (responsibility) {
|
|
1051
|
+
current.responsibility = responsibility;
|
|
1052
|
+
}
|
|
1053
|
+
linkedTestCaseIds.forEach((testCaseId) => current.linkedTestCaseIds.add(testCaseId));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private getMewpWorkItemType(workItem: any): string {
|
|
1058
|
+
return this.toMewpComparableText(workItem?.fields?.['System.WorkItemType']);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private isMewpRequirementType(workItemType: string, requirementTypeSet: Set<string>): boolean {
|
|
1062
|
+
const normalized = String(workItemType || '')
|
|
1063
|
+
.trim()
|
|
1064
|
+
.toLowerCase();
|
|
1065
|
+
if (!normalized) return false;
|
|
1066
|
+
if (requirementTypeSet.has(normalized)) return true;
|
|
1067
|
+
return normalized.includes('requirement') || normalized === 'epic';
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private isMewpTestCaseType(workItemType: string): boolean {
|
|
1071
|
+
const normalized = String(workItemType || '')
|
|
1072
|
+
.trim()
|
|
1073
|
+
.toLowerCase();
|
|
1074
|
+
return normalized === 'test case' || normalized === 'testcase';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
|
|
1078
|
+
try {
|
|
1079
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workitemtypes?api-version=7.1-preview.2`;
|
|
1080
|
+
const result = await TFSServices.getItemContent(url, this.token);
|
|
1081
|
+
const values = Array.isArray(result?.value) ? result.value : [];
|
|
1082
|
+
const matched = values
|
|
1083
|
+
.map((item: any) => String(item?.name || ''))
|
|
1084
|
+
.filter((name: string) => /requirement/i.test(name) || /^epic$/i.test(name));
|
|
1085
|
+
const unique = Array.from(new Set<string>(matched));
|
|
1086
|
+
if (unique.length > 0) {
|
|
1087
|
+
return unique;
|
|
1088
|
+
}
|
|
1089
|
+
} catch (error: any) {
|
|
1090
|
+
logger.debug(`Could not fetch MEWP work item types, using defaults: ${error?.message || error}`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return ['Requirement', 'Epic'];
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private async fetchWorkItemsByIds(
|
|
1097
|
+
projectName: string,
|
|
1098
|
+
workItemIds: number[],
|
|
1099
|
+
includeRelations: boolean
|
|
1100
|
+
): Promise<any[]> {
|
|
1101
|
+
const ids = [...new Set(workItemIds.filter((id) => Number.isFinite(id)))];
|
|
1102
|
+
if (ids.length === 0) return [];
|
|
1103
|
+
|
|
1104
|
+
const CHUNK_SIZE = 200;
|
|
1105
|
+
const allItems: any[] = [];
|
|
1106
|
+
|
|
1107
|
+
for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
|
|
1108
|
+
const chunk = ids.slice(i, i + CHUNK_SIZE);
|
|
1109
|
+
const idsQuery = chunk.join(',');
|
|
1110
|
+
const expandParam = includeRelations ? '&$expand=relations' : '';
|
|
1111
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workitems?ids=${idsQuery}${expandParam}&api-version=7.1-preview.3`;
|
|
1112
|
+
const response = await TFSServices.getItemContent(url, this.token);
|
|
1113
|
+
const values = Array.isArray(response?.value) ? response.value : [];
|
|
1114
|
+
allItems.push(...values);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return allItems;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private extractLinkedTestCaseIdsFromRequirement(relations: any[]): number[] {
|
|
1121
|
+
const out = new Set<number>();
|
|
1122
|
+
for (const relation of Array.isArray(relations) ? relations : []) {
|
|
1123
|
+
const rel = String(relation?.rel || '')
|
|
1124
|
+
.trim()
|
|
1125
|
+
.toLowerCase();
|
|
1126
|
+
const isRequirementToTestLink = rel.includes('testedby') || rel.includes('.tests');
|
|
1127
|
+
if (!isRequirementToTestLink) continue;
|
|
1128
|
+
|
|
1129
|
+
const url = String(relation?.url || '');
|
|
1130
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
1131
|
+
if (!match) continue;
|
|
1132
|
+
const id = Number(match[1]);
|
|
1133
|
+
if (Number.isFinite(id)) out.add(id);
|
|
1134
|
+
}
|
|
1135
|
+
return [...out].sort((a, b) => a - b);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private extractMewpRequirementIdentifier(fields: Record<string, any>, fallbackWorkItemId: number): string {
|
|
1139
|
+
const entries = Object.entries(fields || {});
|
|
1140
|
+
|
|
1141
|
+
// First pass: only trusted identifier-like fields.
|
|
1142
|
+
const strictHints = [
|
|
1143
|
+
'customerid',
|
|
1144
|
+
'customer id',
|
|
1145
|
+
'customerrequirementid',
|
|
1146
|
+
'requirementid',
|
|
1147
|
+
'externalid',
|
|
1148
|
+
'srid',
|
|
1149
|
+
'sapwbsid',
|
|
1150
|
+
];
|
|
1151
|
+
for (const [key, value] of entries) {
|
|
1152
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
1153
|
+
if (!strictHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
1154
|
+
|
|
1155
|
+
const valueAsString = this.toMewpComparableText(value);
|
|
1156
|
+
if (!valueAsString) continue;
|
|
1157
|
+
const normalized = this.normalizeMewpRequirementCode(valueAsString);
|
|
1158
|
+
if (normalized) return normalized;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Second pass: weaker hints, but still key-based only.
|
|
1162
|
+
const looseHints = ['customer', 'requirement', 'external', 'sapwbs', 'sr'];
|
|
1163
|
+
for (const [key, value] of entries) {
|
|
1164
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
1165
|
+
if (!looseHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
1166
|
+
|
|
1167
|
+
const valueAsString = this.toMewpComparableText(value);
|
|
1168
|
+
if (!valueAsString) continue;
|
|
1169
|
+
const normalized = this.normalizeMewpRequirementCode(valueAsString);
|
|
1170
|
+
if (normalized) return normalized;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Optional fallback from title only (avoid scanning all fields and accidental SR matches).
|
|
1174
|
+
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
1175
|
+
const titleCode = this.normalizeMewpRequirementCode(title);
|
|
1176
|
+
if (titleCode) return titleCode;
|
|
1177
|
+
|
|
1178
|
+
return String(fallbackWorkItemId || '');
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private deriveMewpResponsibility(fields: Record<string, any>): string {
|
|
1182
|
+
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
1183
|
+
const fromAreaPath = this.resolveMewpResponsibility(areaPath);
|
|
1184
|
+
if (fromAreaPath) return fromAreaPath;
|
|
1185
|
+
|
|
1186
|
+
const keyHints = ['sapwbs', 'responsibility', 'owner'];
|
|
1187
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
1188
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
1189
|
+
if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
1190
|
+
const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
1191
|
+
if (resolved) return resolved;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return '';
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private resolveMewpResponsibility(value: string): string {
|
|
1198
|
+
const text = String(value || '')
|
|
1199
|
+
.trim()
|
|
1200
|
+
.toLowerCase();
|
|
1201
|
+
if (!text) return '';
|
|
1202
|
+
|
|
1203
|
+
if (/(^|[^a-z0-9])esuk([^a-z0-9]|$)/i.test(text)) return 'ESUK';
|
|
1204
|
+
if (/(^|[^a-z0-9])il([^a-z0-9]|$)/i.test(text)) return 'IL';
|
|
1205
|
+
return '';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private normalizeMewpRequirementCode(value: string): string {
|
|
1209
|
+
const text = String(value || '').trim();
|
|
1210
|
+
if (!text) return '';
|
|
1211
|
+
const match = /\bSR[\s\-_]*([0-9]+)\b/i.exec(text);
|
|
1212
|
+
if (!match) return '';
|
|
1213
|
+
return `SR${match[1]}`;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private toMewpComparableText(value: any): string {
|
|
1217
|
+
if (value === null || value === undefined) return '';
|
|
1218
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
1219
|
+
return String(value).trim();
|
|
1220
|
+
}
|
|
1221
|
+
if (typeof value === 'object') {
|
|
1222
|
+
const displayName = (value as any).displayName;
|
|
1223
|
+
if (displayName) return String(displayName).trim();
|
|
1224
|
+
const name = (value as any).name;
|
|
1225
|
+
if (name) return String(name).trim();
|
|
1226
|
+
const uniqueName = (value as any).uniqueName;
|
|
1227
|
+
if (uniqueName) return String(uniqueName).trim();
|
|
1228
|
+
}
|
|
1229
|
+
return String(value).trim();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
416
1232
|
private async fetchTestPlanName(testPlanId: string, teamProject: string): Promise<string> {
|
|
417
1233
|
try {
|
|
418
1234
|
const url = `${this.orgUrl}${teamProject}/_apis/testplan/Plans/${testPlanId}?api-version=5.1`;
|
|
@@ -1880,13 +2696,22 @@ export default class ResultDataProvider {
|
|
|
1880
2696
|
...additionalArgs
|
|
1881
2697
|
);
|
|
1882
2698
|
|
|
1883
|
-
|
|
2699
|
+
let iteration =
|
|
1884
2700
|
resultData.iterationDetails?.length > 0
|
|
1885
2701
|
? resultData.iterationDetails[resultData.iterationDetails.length - 1]
|
|
1886
2702
|
: undefined;
|
|
1887
2703
|
|
|
2704
|
+
if (resultData.stepsResultXml && !iteration) {
|
|
2705
|
+
iteration = { actionResults: [] };
|
|
2706
|
+
if (!Array.isArray(resultData.iterationDetails)) {
|
|
2707
|
+
resultData.iterationDetails = [];
|
|
2708
|
+
}
|
|
2709
|
+
resultData.iterationDetails.push(iteration);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
1888
2712
|
if (resultData.stepsResultXml && iteration) {
|
|
1889
|
-
const
|
|
2713
|
+
const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
|
|
2714
|
+
const actionResultsWithSharedModels = actionResults.filter(
|
|
1890
2715
|
(result: any) => result.sharedStepModel
|
|
1891
2716
|
);
|
|
1892
2717
|
|
|
@@ -1910,7 +2735,7 @@ export default class ResultDataProvider {
|
|
|
1910
2735
|
stepMap.set(step.stepId.toString(), step);
|
|
1911
2736
|
}
|
|
1912
2737
|
|
|
1913
|
-
for (const actionResult of
|
|
2738
|
+
for (const actionResult of actionResults) {
|
|
1914
2739
|
const step = stepMap.get(actionResult.stepIdentifier);
|
|
1915
2740
|
if (step) {
|
|
1916
2741
|
actionResult.stepPosition = step.stepPosition;
|
|
@@ -1919,10 +2744,28 @@ export default class ResultDataProvider {
|
|
|
1919
2744
|
actionResult.isSharedStepTitle = step.isSharedStepTitle;
|
|
1920
2745
|
}
|
|
1921
2746
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
.
|
|
2747
|
+
|
|
2748
|
+
if (actionResults.length > 0) {
|
|
2749
|
+
// Sort mapped action results by logical step position.
|
|
2750
|
+
iteration.actionResults = actionResults
|
|
2751
|
+
.filter((result: any) => result.stepPosition)
|
|
2752
|
+
.sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
|
|
2753
|
+
} else {
|
|
2754
|
+
// Fallback for runs that have no action results: emit test definition steps as Not Run.
|
|
2755
|
+
iteration.actionResults = stepsList
|
|
2756
|
+
.filter((step: any) => step?.stepPosition)
|
|
2757
|
+
.map((step: any) => ({
|
|
2758
|
+
stepIdentifier: String(step?.stepId ?? step?.stepPosition ?? ''),
|
|
2759
|
+
stepPosition: step.stepPosition,
|
|
2760
|
+
action: step.action,
|
|
2761
|
+
expected: step.expected,
|
|
2762
|
+
isSharedStepTitle: step.isSharedStepTitle,
|
|
2763
|
+
outcome: 'Unspecified',
|
|
2764
|
+
errorMessage: '',
|
|
2765
|
+
actionPath: String(step?.stepPosition ?? ''),
|
|
2766
|
+
}))
|
|
2767
|
+
.sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
|
|
2768
|
+
}
|
|
1926
2769
|
}
|
|
1927
2770
|
|
|
1928
2771
|
return resultData?.testCase
|