@elisra-devops/docgen-data-provider 1.101.0 → 1.103.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/models/mewp-reporting.d.ts +5 -0
- package/bin/modules/ResultDataProvider.d.ts +34 -0
- package/bin/modules/ResultDataProvider.js +213 -90
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +335 -1
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/models/mewp-reporting.ts +5 -0
- package/src/modules/ResultDataProvider.ts +229 -115
- package/src/tests/modules/ResultDataProvider.test.ts +389 -1
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ export interface MewpL2RequirementWorkItem {
|
|
|
28
28
|
requirementId: string;
|
|
29
29
|
baseKey: string;
|
|
30
30
|
title: string;
|
|
31
|
+
owner: string;
|
|
31
32
|
subSystem: string;
|
|
32
33
|
responsibility: string;
|
|
33
34
|
linkedTestCaseIds: number[];
|
|
@@ -40,6 +41,7 @@ export interface MewpL2RequirementFamily {
|
|
|
40
41
|
requirementId: string;
|
|
41
42
|
baseKey: string;
|
|
42
43
|
title: string;
|
|
44
|
+
owner: string;
|
|
43
45
|
subSystem: string;
|
|
44
46
|
responsibility: string;
|
|
45
47
|
linkedTestCaseIds: number[];
|
|
@@ -84,7 +86,10 @@ export type MewpCoverageL3L4Cell = MewpL3L4Pair;
|
|
|
84
86
|
|
|
85
87
|
export interface MewpCoverageRow {
|
|
86
88
|
'L2 REQ ID': string;
|
|
89
|
+
'SR #': string;
|
|
87
90
|
'L2 REQ Title': string;
|
|
91
|
+
'L2 REQ Full Title'?: string;
|
|
92
|
+
'L2 Owner': string;
|
|
88
93
|
'L2 SubSystem': string;
|
|
89
94
|
'L2 Run Status': MewpRunStatus;
|
|
90
95
|
'Bug ID': number | '';
|
|
@@ -56,10 +56,14 @@ export default class ResultDataProvider {
|
|
|
56
56
|
private static readonly MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG =
|
|
57
57
|
'[MEWP][InternalValidation][Diagnostics]';
|
|
58
58
|
private static readonly MEWP_INTERNAL_VALIDATION_SUMMARY_TAG = '[MEWP][InternalValidation][Summary]';
|
|
59
|
+
private static readonly MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_REF = 'Assumptions';
|
|
60
|
+
private static readonly MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN = /assumptions/i;
|
|
59
61
|
|
|
60
62
|
private static readonly MEWP_L2_COVERAGE_COLUMNS = [
|
|
61
63
|
'L2 REQ ID',
|
|
64
|
+
'SR #',
|
|
62
65
|
'L2 REQ Title',
|
|
66
|
+
'L2 Owner',
|
|
63
67
|
'L2 SubSystem',
|
|
64
68
|
'L2 Run Status',
|
|
65
69
|
'Bug ID',
|
|
@@ -806,6 +810,7 @@ export default class ResultDataProvider {
|
|
|
806
810
|
const rows: MewpInternalValidationRow[] = [];
|
|
807
811
|
const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
|
|
808
812
|
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
813
|
+
const testCaseDescriptionMap = this.buildMewpTestCaseDescriptionMap(testData);
|
|
809
814
|
const allTestCaseIds = new Set<number>();
|
|
810
815
|
for (const suite of testData || []) {
|
|
811
816
|
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
@@ -861,7 +866,16 @@ export default class ResultDataProvider {
|
|
|
861
866
|
: [];
|
|
862
867
|
const executableSteps = parsedSteps.filter((step) => !step?.isSharedStepTitle);
|
|
863
868
|
diagnostics.totalParsedSteps += executableSteps.length;
|
|
864
|
-
|
|
869
|
+
// Direction A/B mentions can come from two sources:
|
|
870
|
+
// 1) Expected Result in executable steps.
|
|
871
|
+
// 2) "Assumptions" section in the test-case description.
|
|
872
|
+
const stepMentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
|
|
873
|
+
const descriptionText = testCaseDescriptionMap.get(testCaseId) || '';
|
|
874
|
+
const assumptionsMentionEntries = this.extractRequirementMentionsFromAssumptionsDescription(
|
|
875
|
+
descriptionText,
|
|
876
|
+
true
|
|
877
|
+
);
|
|
878
|
+
const mentionEntries = [...stepMentionEntries, ...assumptionsMentionEntries];
|
|
865
879
|
diagnostics.totalStepsWithMentions += mentionEntries.length;
|
|
866
880
|
const mentionedL2Only = new Set<string>();
|
|
867
881
|
const mentionedCodeFirstStep = new Map<string, string>();
|
|
@@ -903,24 +917,6 @@ export default class ResultDataProvider {
|
|
|
903
917
|
if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
|
|
904
918
|
mentionedCodesByBase.get(baseKey)!.add(code);
|
|
905
919
|
}
|
|
906
|
-
if (traceCurrentTestCase) {
|
|
907
|
-
logger.debug(
|
|
908
|
-
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
|
|
909
|
-
event: 'test-case-start',
|
|
910
|
-
tc: testCaseId,
|
|
911
|
-
parsedSteps: executableSteps.length,
|
|
912
|
-
stepsWithMentions: mentionEntries.length,
|
|
913
|
-
mentionedCodes:
|
|
914
|
-
[...mentionedL2Only].sort((a, b) => this.compareMewpRequirementCodes(a, b)).join('; ') ||
|
|
915
|
-
'<none>',
|
|
916
|
-
linkedCodesInTestCase:
|
|
917
|
-
[...linkedFullCodes]
|
|
918
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
919
|
-
.join('; ') || '<none>',
|
|
920
|
-
})
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
920
|
// Direction A logic:
|
|
925
921
|
// 1) Base mention ("SR0054") is parent-level and considered covered only when
|
|
926
922
|
// the whole family is covered across scoped test cases:
|
|
@@ -946,38 +942,12 @@ export default class ResultDataProvider {
|
|
|
946
942
|
|
|
947
943
|
// Base mention ("SR0054") requires full family coverage across selected test cases.
|
|
948
944
|
if (hasBaseMention) {
|
|
949
|
-
const missingRequiredFamilyMembers = requiredFamilyMembers.filter(
|
|
950
|
-
(memberCode) => !familyLinkedCodes.has(memberCode)
|
|
951
|
-
);
|
|
952
945
|
const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
|
|
953
946
|
familyLinkedCodes.has(memberCode)
|
|
954
947
|
);
|
|
955
948
|
if (!isWholeFamilyCovered) {
|
|
956
949
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
957
950
|
}
|
|
958
|
-
if (traceCurrentTestCase) {
|
|
959
|
-
logger.debug(
|
|
960
|
-
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
|
|
961
|
-
event: 'base-family-coverage',
|
|
962
|
-
tc: testCaseId,
|
|
963
|
-
base: baseKey,
|
|
964
|
-
baseMention: true,
|
|
965
|
-
requiredFamily:
|
|
966
|
-
requiredFamilyMembers
|
|
967
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
968
|
-
.join('; ') || '<none>',
|
|
969
|
-
linkedAcrossScope:
|
|
970
|
-
[...familyLinkedCodes]
|
|
971
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
972
|
-
.join('; ') || '<none>',
|
|
973
|
-
missingRequired:
|
|
974
|
-
missingRequiredFamilyMembers
|
|
975
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
976
|
-
.join('; ') || '<none>',
|
|
977
|
-
covered: isWholeFamilyCovered,
|
|
978
|
-
})
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
951
|
}
|
|
982
952
|
|
|
983
953
|
// Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
|
|
@@ -987,60 +957,20 @@ export default class ResultDataProvider {
|
|
|
987
957
|
for (const code of missingSpecificMembers) {
|
|
988
958
|
missingSpecificMentionedNoFamily.add(code);
|
|
989
959
|
}
|
|
990
|
-
if (traceCurrentTestCase && mentionedSpecificMembers.length > 0) {
|
|
991
|
-
logger.debug(
|
|
992
|
-
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
|
|
993
|
-
event: 'specific-members-check',
|
|
994
|
-
tc: testCaseId,
|
|
995
|
-
base: baseKey,
|
|
996
|
-
specificMentioned:
|
|
997
|
-
mentionedSpecificMembers
|
|
998
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
999
|
-
.join('; ') || '<none>',
|
|
1000
|
-
specificMissing:
|
|
1001
|
-
missingSpecificMembers
|
|
1002
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
1003
|
-
.join('; ') || '<none>',
|
|
1004
|
-
})
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
960
|
continue;
|
|
1008
961
|
}
|
|
1009
962
|
|
|
1010
963
|
// Fallback path when family data is unavailable for this base key.
|
|
1011
|
-
const fallbackMissingSpecific: string[] = [];
|
|
1012
|
-
let fallbackMissingBase = false;
|
|
1013
964
|
for (const code of mentionedCodes) {
|
|
1014
965
|
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
1015
966
|
if (hasSpecificSuffix) {
|
|
1016
967
|
if (!linkedFullCodesAcrossTestCases.has(code)) {
|
|
1017
968
|
missingSpecificMentionedNoFamily.add(code);
|
|
1018
|
-
fallbackMissingSpecific.push(code);
|
|
1019
969
|
}
|
|
1020
970
|
} else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
|
|
1021
971
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
1022
|
-
fallbackMissingBase = true;
|
|
1023
972
|
}
|
|
1024
973
|
}
|
|
1025
|
-
if (traceCurrentTestCase) {
|
|
1026
|
-
logger.debug(
|
|
1027
|
-
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
|
|
1028
|
-
event: 'fallback-path',
|
|
1029
|
-
tc: testCaseId,
|
|
1030
|
-
base: baseKey,
|
|
1031
|
-
fallbackUsed: true,
|
|
1032
|
-
mentioned:
|
|
1033
|
-
mentionedCodesList
|
|
1034
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
1035
|
-
.join('; ') || '<none>',
|
|
1036
|
-
missingSpecific:
|
|
1037
|
-
fallbackMissingSpecific
|
|
1038
|
-
.sort((a, b) => this.compareMewpRequirementCodes(a, b))
|
|
1039
|
-
.join('; ') || '<none>',
|
|
1040
|
-
missingBase: fallbackMissingBase,
|
|
1041
|
-
})
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
974
|
}
|
|
1045
975
|
|
|
1046
976
|
// Direction B is family-based: if any member of a family is mentioned in Expected Result,
|
|
@@ -1086,17 +1016,6 @@ export default class ResultDataProvider {
|
|
|
1086
1016
|
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
1087
1017
|
appendMentionedButNotLinked(baseKey, stepRef);
|
|
1088
1018
|
}
|
|
1089
|
-
if (traceCurrentTestCase) {
|
|
1090
|
-
logger.debug(
|
|
1091
|
-
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
|
|
1092
|
-
event: 'direction-a-summary',
|
|
1093
|
-
tc: testCaseId,
|
|
1094
|
-
missingSpecific: sortedMissingSpecificMentionedNoFamily.join('; ') || '<none>',
|
|
1095
|
-
missingBase: sortedMissingBaseWhenFamilyUncovered.join('; ') || '<none>',
|
|
1096
|
-
})
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
1019
|
const sortedExtraLinked = [...new Set(extraLinked)]
|
|
1101
1020
|
.map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
|
|
1102
1021
|
.filter((code) => !!code)
|
|
@@ -1134,21 +1053,23 @@ export default class ResultDataProvider {
|
|
|
1134
1053
|
const validationStatus: 'Pass' | 'Fail' =
|
|
1135
1054
|
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
1136
1055
|
if (validationStatus === 'Fail') diagnostics.failingRows += 1;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1056
|
+
if (traceCurrentTestCase) {
|
|
1057
|
+
logger.debug(
|
|
1058
|
+
this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
|
|
1059
|
+
testCaseId,
|
|
1060
|
+
parsedSteps: executableSteps.length,
|
|
1061
|
+
stepsWithMentions: mentionEntries.length,
|
|
1062
|
+
customerIdsFound: mentionedL2Only.size,
|
|
1063
|
+
linkedRequirements: linkedFullCodes.size,
|
|
1064
|
+
mentionedButNotLinked:
|
|
1065
|
+
sortedMissingSpecificMentionedNoFamily.length +
|
|
1066
|
+
sortedMissingBaseWhenFamilyUncovered.length,
|
|
1067
|
+
linkedButNotMentioned: sortedExtraLinked.length,
|
|
1068
|
+
status: validationStatus,
|
|
1069
|
+
customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
|
|
1070
|
+
})
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1152
1073
|
|
|
1153
1074
|
rows.push({
|
|
1154
1075
|
'Test Case ID': testCaseId,
|
|
@@ -1297,7 +1218,7 @@ export default class ResultDataProvider {
|
|
|
1297
1218
|
private createMewpCoverageRow(
|
|
1298
1219
|
requirement: Pick<
|
|
1299
1220
|
MewpL2RequirementFamily,
|
|
1300
|
-
'workItemId' | 'requirementId' | 'title' | 'subSystem' | 'responsibility'
|
|
1221
|
+
'workItemId' | 'requirementId' | 'title' | 'owner' | 'subSystem' | 'responsibility'
|
|
1301
1222
|
>,
|
|
1302
1223
|
runStatus: MewpRunStatus,
|
|
1303
1224
|
bug: MewpCoverageBugCell,
|
|
@@ -1305,12 +1226,18 @@ export default class ResultDataProvider {
|
|
|
1305
1226
|
): MewpCoverageRow {
|
|
1306
1227
|
const l2ReqIdNumeric = Number(requirement?.workItemId || 0);
|
|
1307
1228
|
const l2ReqId = l2ReqIdNumeric > 0 ? String(l2ReqIdNumeric) : '';
|
|
1229
|
+
const srNumber = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
|
|
1308
1230
|
const l2ReqTitle = this.toMewpComparableText(requirement.title);
|
|
1231
|
+
const reqName = this.deriveMewpRequirementDisplayName(srNumber, l2ReqTitle);
|
|
1232
|
+
const l2Owner = this.toMewpComparableText(requirement.owner);
|
|
1309
1233
|
const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
|
|
1310
1234
|
|
|
1311
1235
|
return {
|
|
1312
1236
|
'L2 REQ ID': l2ReqId,
|
|
1313
|
-
'
|
|
1237
|
+
'SR #': srNumber,
|
|
1238
|
+
'L2 REQ Title': reqName,
|
|
1239
|
+
'L2 REQ Full Title': l2ReqTitle,
|
|
1240
|
+
'L2 Owner': l2Owner,
|
|
1314
1241
|
'L2 SubSystem': l2SubSystem,
|
|
1315
1242
|
'L2 Run Status': runStatus,
|
|
1316
1243
|
'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
|
|
@@ -1323,6 +1250,20 @@ export default class ResultDataProvider {
|
|
|
1323
1250
|
};
|
|
1324
1251
|
}
|
|
1325
1252
|
|
|
1253
|
+
private deriveMewpRequirementDisplayName(requirementCode: string, title: string): string {
|
|
1254
|
+
const normalizedTitle = this.toMewpComparableText(title);
|
|
1255
|
+
if (!normalizedTitle) return '';
|
|
1256
|
+
|
|
1257
|
+
const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(requirementCode || '');
|
|
1258
|
+
if (!normalizedCode) return normalizedTitle;
|
|
1259
|
+
|
|
1260
|
+
const escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1261
|
+
const codePrefixPattern = new RegExp(`^${escapedCode}(?:\\s*[:\\-–—]\\s*|\\s+)`, 'i');
|
|
1262
|
+
const withoutCodePrefix = normalizedTitle.replace(codePrefixPattern, '').trim();
|
|
1263
|
+
if (!withoutCodePrefix) return normalizedTitle;
|
|
1264
|
+
return withoutCodePrefix;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1326
1267
|
private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
|
|
1327
1268
|
return { id: '' as '', title: '', responsibility: '' };
|
|
1328
1269
|
}
|
|
@@ -1643,14 +1584,17 @@ export default class ResultDataProvider {
|
|
|
1643
1584
|
}
|
|
1644
1585
|
|
|
1645
1586
|
private buildRequirementSapWbsByBaseKey(
|
|
1646
|
-
requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
|
|
1587
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'owner' | 'responsibility'>>
|
|
1647
1588
|
): Map<string, string> {
|
|
1648
1589
|
const out = new Map<string, string>();
|
|
1649
1590
|
for (const requirement of requirements || []) {
|
|
1650
1591
|
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1651
1592
|
if (!baseKey) continue;
|
|
1652
1593
|
|
|
1653
|
-
const
|
|
1594
|
+
const rawOwner = this.toMewpComparableText(requirement?.owner);
|
|
1595
|
+
const normalized =
|
|
1596
|
+
this.resolveMewpResponsibility(rawOwner) ||
|
|
1597
|
+
this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
|
|
1654
1598
|
if (!normalized) continue;
|
|
1655
1599
|
|
|
1656
1600
|
const existing = out.get(baseKey) || '';
|
|
@@ -1752,6 +1696,54 @@ export default class ResultDataProvider {
|
|
|
1752
1696
|
return map;
|
|
1753
1697
|
}
|
|
1754
1698
|
|
|
1699
|
+
/**
|
|
1700
|
+
* Builds a lookup of test case id -> test case description using suite payload data.
|
|
1701
|
+
*
|
|
1702
|
+
* Resolution order:
|
|
1703
|
+
* 1) direct description fields on test-case payload (`testCase.description` / `workItem.description`)
|
|
1704
|
+
* 2) `System.Description` / `Description` in work-item fields map
|
|
1705
|
+
* 3) `System.Description` / `Description` from work-item field list (`workItemFields`)
|
|
1706
|
+
*/
|
|
1707
|
+
private buildMewpTestCaseDescriptionMap(testData: any[]): Map<number, string> {
|
|
1708
|
+
const map = new Map<number, string>();
|
|
1709
|
+
|
|
1710
|
+
const readDescriptionFromFields = (fields: Record<string, any>): string => {
|
|
1711
|
+
const value =
|
|
1712
|
+
this.getFieldValueByName(fields, 'System.Description') ??
|
|
1713
|
+
this.getFieldValueByName(fields, 'Description');
|
|
1714
|
+
return this.toMewpComparableText(value);
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
for (const suite of testData || []) {
|
|
1718
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1719
|
+
for (const testCase of testCasesItems) {
|
|
1720
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id);
|
|
1721
|
+
if (!Number.isFinite(id) || id <= 0 || map.has(id)) continue;
|
|
1722
|
+
|
|
1723
|
+
const fromDirect = this.toMewpComparableText(testCase?.description || testCase?.workItem?.description);
|
|
1724
|
+
if (fromDirect) {
|
|
1725
|
+
map.set(id, fromDirect);
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const fieldsFromMap = testCase?.workItem?.fields || {};
|
|
1730
|
+
const fromFieldsMap = readDescriptionFromFields(fieldsFromMap);
|
|
1731
|
+
if (fromFieldsMap) {
|
|
1732
|
+
map.set(id, fromFieldsMap);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const fieldsFromList = this.extractWorkItemFieldsMap(testCase?.workItem?.workItemFields);
|
|
1737
|
+
const fromFieldsList = readDescriptionFromFields(fieldsFromList);
|
|
1738
|
+
if (fromFieldsList) {
|
|
1739
|
+
map.set(id, fromFieldsList);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
return map;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1755
1747
|
private extractMewpTestCaseId(runResult: any): number {
|
|
1756
1748
|
const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
|
|
1757
1749
|
return Number.isFinite(testCaseId) ? testCaseId : 0;
|
|
@@ -1956,6 +1948,107 @@ export default class ResultDataProvider {
|
|
|
1956
1948
|
return out;
|
|
1957
1949
|
}
|
|
1958
1950
|
|
|
1951
|
+
/**
|
|
1952
|
+
* Extracts SR requirement mentions from the "assumptions" section inside a test-case description.
|
|
1953
|
+
*
|
|
1954
|
+
* Notes:
|
|
1955
|
+
* - Heading detection is case-insensitive and keyed by "assumptions".
|
|
1956
|
+
* - Parsing is scoped to that section only and stops when a likely next section heading is reached
|
|
1957
|
+
* after at least one requirement-bearing line was collected.
|
|
1958
|
+
* - Returned stepRef is a synthetic marker ("Assumptions") so downstream discrepancy output can
|
|
1959
|
+
* clearly attribute source to description-level assumptions.
|
|
1960
|
+
*/
|
|
1961
|
+
private extractRequirementMentionsFromAssumptionsDescription(
|
|
1962
|
+
description: string,
|
|
1963
|
+
includeSuffix: boolean
|
|
1964
|
+
): Array<{ stepRef: string; codes: Set<string> }> {
|
|
1965
|
+
const lines = this.normalizeMewpDescriptionLines(description);
|
|
1966
|
+
if (lines.length === 0) return [];
|
|
1967
|
+
|
|
1968
|
+
const assumptionsHeaderIndex = lines.findIndex((line) =>
|
|
1969
|
+
ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN.test(line)
|
|
1970
|
+
);
|
|
1971
|
+
if (assumptionsHeaderIndex < 0) return [];
|
|
1972
|
+
|
|
1973
|
+
const assumptionCodes = new Set<string>();
|
|
1974
|
+
let hasCollectedRequirementCodes = false;
|
|
1975
|
+
for (let index = assumptionsHeaderIndex + 1; index < lines.length; index += 1) {
|
|
1976
|
+
const rawLine = String(lines[index] || '').trim();
|
|
1977
|
+
if (!rawLine) continue;
|
|
1978
|
+
|
|
1979
|
+
const line = rawLine.replace(/^[-*•]+\s*/, '').trim();
|
|
1980
|
+
if (!line) continue;
|
|
1981
|
+
|
|
1982
|
+
const lineCodes = this.extractRequirementCodesFromExpectedText(line, includeSuffix);
|
|
1983
|
+
if (lineCodes.size > 0) {
|
|
1984
|
+
for (const code of lineCodes) assumptionCodes.add(code);
|
|
1985
|
+
hasCollectedRequirementCodes = true;
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (hasCollectedRequirementCodes && this.isLikelyMewpDescriptionSectionHeading(line)) {
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (assumptionCodes.size === 0) return [];
|
|
1995
|
+
return [{ stepRef: ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_REF, codes: assumptionCodes }];
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* Normalizes raw HTML/plain description content into comparable text lines.
|
|
2000
|
+
* This makes section/title heuristics resilient to formatting differences.
|
|
2001
|
+
*/
|
|
2002
|
+
private normalizeMewpDescriptionLines(description: string): string[] {
|
|
2003
|
+
const raw = String(description || '');
|
|
2004
|
+
if (!raw) return [];
|
|
2005
|
+
|
|
2006
|
+
const normalized = raw
|
|
2007
|
+
.replace(/\r\n?/g, '\n')
|
|
2008
|
+
.replace(/<\s*br\s*\/?>/gi, '\n')
|
|
2009
|
+
// Underlined sections are commonly used as inline HTML titles in MEWP test-case descriptions.
|
|
2010
|
+
// Treat underline tags as boundaries so compact forms like <u><b>Title</b></u><p>... preserve section split.
|
|
2011
|
+
.replace(/<\s*u\b[^>]*>/gi, '\n')
|
|
2012
|
+
.replace(/<\/\s*u\s*>/gi, '\n')
|
|
2013
|
+
.replace(/<\s*li\b[^>]*>/gi, '\n- ')
|
|
2014
|
+
.replace(/<\/\s*(p|div|li|ul|ol|tr|td|h[1-6])\s*>/gi, '\n')
|
|
2015
|
+
.replace(/ | | /gi, ' ')
|
|
2016
|
+
.replace(/</gi, '<')
|
|
2017
|
+
.replace(/>/gi, '>')
|
|
2018
|
+
.replace(/&/gi, '&')
|
|
2019
|
+
.replace(/"/gi, '"')
|
|
2020
|
+
.replace(/'|'/gi, "'")
|
|
2021
|
+
.replace(/<[^>]*>/g, ' ')
|
|
2022
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
2023
|
+
.replace(/[ \t]+/g, ' ');
|
|
2024
|
+
|
|
2025
|
+
return normalized
|
|
2026
|
+
.split('\n')
|
|
2027
|
+
.map((line) => String(line || '').trim())
|
|
2028
|
+
.filter((line) => !!line);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Heuristic detector for description section headings used to stop assumptions parsing.
|
|
2033
|
+
* A line is considered a heading when it is short/title-like and does not itself contain SR codes.
|
|
2034
|
+
*/
|
|
2035
|
+
private isLikelyMewpDescriptionSectionHeading(line: string): boolean {
|
|
2036
|
+
const normalized = String(line || '')
|
|
2037
|
+
.replace(/^[-*•]+\s*/, '')
|
|
2038
|
+
.trim();
|
|
2039
|
+
if (!normalized) return false;
|
|
2040
|
+
if (ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN.test(normalized))
|
|
2041
|
+
return false;
|
|
2042
|
+
if (this.extractRequirementCodesFromExpectedText(normalized, true).size > 0) return false;
|
|
2043
|
+
|
|
2044
|
+
const compact = normalized.replace(/[.:;,\-–—]+$/, '').trim();
|
|
2045
|
+
if (!compact) return false;
|
|
2046
|
+
const wordCount = compact.split(/\s+/).filter((item) => !!item).length;
|
|
2047
|
+
if (wordCount === 0 || wordCount > 12) return false;
|
|
2048
|
+
if (/[.;!?]/.test(compact)) return false;
|
|
2049
|
+
return /^[a-z0-9\s()&/+_'-]+$/i.test(compact);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1959
2052
|
private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
|
|
1960
2053
|
const out = new Set<string>();
|
|
1961
2054
|
const source = this.normalizeRequirementStepText(text);
|
|
@@ -2138,6 +2231,7 @@ export default class ResultDataProvider {
|
|
|
2138
2231
|
requirementId,
|
|
2139
2232
|
baseKey: this.toRequirementKey(requirementId),
|
|
2140
2233
|
title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
|
|
2234
|
+
owner: this.deriveMewpRequirementOwner(fields),
|
|
2141
2235
|
subSystem: this.deriveMewpSubSystem(fields),
|
|
2142
2236
|
responsibility: this.deriveMewpResponsibility(fields),
|
|
2143
2237
|
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
|
|
@@ -2187,6 +2281,7 @@ export default class ResultDataProvider {
|
|
|
2187
2281
|
if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
|
|
2188
2282
|
if (!areaPath.includes('\\mop')) score += 2;
|
|
2189
2283
|
if (String(item?.title || '').trim()) score += 1;
|
|
2284
|
+
if (String(item?.owner || '').trim()) score += 1;
|
|
2190
2285
|
if (String(item?.subSystem || '').trim()) score += 1;
|
|
2191
2286
|
if (String(item?.responsibility || '').trim()) score += 1;
|
|
2192
2287
|
return score;
|
|
@@ -2223,6 +2318,7 @@ export default class ResultDataProvider {
|
|
|
2223
2318
|
requirementId: String(family?.representative?.requirementId || baseKey),
|
|
2224
2319
|
baseKey,
|
|
2225
2320
|
title: String(family?.representative?.title || ''),
|
|
2321
|
+
owner: String(family?.representative?.owner || ''),
|
|
2226
2322
|
subSystem: String(family?.representative?.subSystem || ''),
|
|
2227
2323
|
responsibility: String(family?.representative?.responsibility || ''),
|
|
2228
2324
|
linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
|
|
@@ -2674,6 +2770,24 @@ export default class ResultDataProvider {
|
|
|
2674
2770
|
return '';
|
|
2675
2771
|
}
|
|
2676
2772
|
|
|
2773
|
+
// L2 owner is sourced only from requirement SAPWBS fields.
|
|
2774
|
+
private deriveMewpRequirementOwner(fields: Record<string, any>): string {
|
|
2775
|
+
const directCandidates = [fields?.['Custom.SAPWBS'], fields?.['SAPWBS']];
|
|
2776
|
+
for (const candidate of directCandidates) {
|
|
2777
|
+
const normalized = this.toMewpComparableText(candidate);
|
|
2778
|
+
if (normalized) return normalized;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
2782
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
2783
|
+
if (!normalizedKey.includes('sapwbs')) continue;
|
|
2784
|
+
const normalizedValue = this.toMewpComparableText(value);
|
|
2785
|
+
if (normalizedValue) return normalizedValue;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
return '';
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2677
2791
|
// Test-case responsibility must come from test-case path context (not SAPWBS).
|
|
2678
2792
|
private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
|
|
2679
2793
|
const areaPathCandidates = [
|