@elisra-devops/docgen-data-provider 1.74.0 → 1.75.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.
@@ -393,8 +393,7 @@ export default class ResultDataProvider {
393
393
  public async getMewpL2CoverageFlatResults(
394
394
  testPlanId: string,
395
395
  projectName: string,
396
- selectedSuiteIds: number[] | undefined,
397
- linkedQueryRequest?: any
396
+ selectedSuiteIds: number[] | undefined
398
397
  ) {
399
398
  const defaultPayload = {
400
399
  sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
@@ -407,7 +406,7 @@ export default class ResultDataProvider {
407
406
  const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
408
407
  const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
409
408
 
410
- const requirements = await this.fetchMewpL2Requirements(projectName, linkedQueryRequest);
409
+ const requirements = await this.fetchMewpL2Requirements(projectName);
411
410
  if (requirements.length === 0) {
412
411
  return {
413
412
  ...defaultPayload,
@@ -560,9 +559,9 @@ export default class ResultDataProvider {
560
559
  testCaseTitle: string,
561
560
  stepSummary: { passed: number; failed: number; notRun: number }
562
561
  ) {
563
- const customerId = String(requirement.requirementId || '').trim();
564
- const customerTitle = String(requirement.title || '').trim();
565
- const responsibility = String(requirement.responsibility || '').trim();
562
+ const customerId = this.formatMewpCustomerId(requirement.requirementId);
563
+ const customerTitle = this.toMewpComparableText(requirement.title);
564
+ const responsibility = this.toMewpComparableText(requirement.responsibility);
566
565
  const safeTestCaseId = Number.isFinite(testCaseId) && Number(testCaseId) > 0 ? Number(testCaseId) : '';
567
566
 
568
567
  return {
@@ -577,6 +576,15 @@ export default class ResultDataProvider {
577
576
  };
578
577
  }
579
578
 
579
+ private formatMewpCustomerId(rawValue: string): string {
580
+ const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
581
+ if (normalized) return normalized;
582
+
583
+ const onlyDigits = String(rawValue || '').replace(/\D/g, '');
584
+ if (onlyDigits) return `SR${onlyDigits}`;
585
+ return '';
586
+ }
587
+
580
588
  private buildMewpCoverageRows(
581
589
  requirements: Array<{
582
590
  requirementId: string;
@@ -802,7 +810,7 @@ export default class ResultDataProvider {
802
810
  return `SR${digits}`;
803
811
  }
804
812
 
805
- private async fetchMewpL2Requirements(projectName: string, linkedQueryRequest?: any): Promise<
813
+ private async fetchMewpL2Requirements(projectName: string): Promise<
806
814
  Array<{
807
815
  workItemId: number;
808
816
  requirementId: string;
@@ -811,11 +819,6 @@ export default class ResultDataProvider {
811
819
  linkedTestCaseIds: number[];
812
820
  }>
813
821
  > {
814
- const queryHref = this.extractMewpQueryHref(linkedQueryRequest);
815
- if (queryHref) {
816
- return this.fetchMewpL2RequirementsFromQuery(projectName, queryHref);
817
- }
818
-
819
822
  const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
820
823
  if (workItemTypeNames.length === 0) {
821
824
  return [];
@@ -846,7 +849,7 @@ ORDER BY [System.Id]`;
846
849
  return {
847
850
  workItemId: Number(wi?.id || 0),
848
851
  requirementId: this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0)),
849
- title: String(fields['System.Title'] || ''),
852
+ title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
850
853
  responsibility: this.deriveMewpResponsibility(fields),
851
854
  linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
852
855
  };
@@ -855,225 +858,6 @@ ORDER BY [System.Id]`;
855
858
  return requirements.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
856
859
  }
857
860
 
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
861
  private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
1078
862
  try {
1079
863
  const url = `${this.orgUrl}${projectName}/_apis/wit/workitemtypes?api-version=7.1-preview.2`;
@@ -1175,10 +959,20 @@ ORDER BY [System.Id]`;
1175
959
  const titleCode = this.normalizeMewpRequirementCode(title);
1176
960
  if (titleCode) return titleCode;
1177
961
 
1178
- return String(fallbackWorkItemId || '');
962
+ return fallbackWorkItemId ? `SR${fallbackWorkItemId}` : '';
1179
963
  }
1180
964
 
1181
965
  private deriveMewpResponsibility(fields: Record<string, any>): string {
966
+ const explicitSapWbs = this.toMewpComparableText(fields?.['Custom.SAPWBS']);
967
+ const fromExplicitSapWbs = this.resolveMewpResponsibility(explicitSapWbs);
968
+ if (fromExplicitSapWbs) return fromExplicitSapWbs;
969
+ if (explicitSapWbs) return explicitSapWbs;
970
+
971
+ const explicitSapWbsByLabel = this.toMewpComparableText(fields?.['SAPWBS']);
972
+ const fromExplicitLabel = this.resolveMewpResponsibility(explicitSapWbsByLabel);
973
+ if (fromExplicitLabel) return fromExplicitLabel;
974
+ if (explicitSapWbsByLabel) return explicitSapWbsByLabel;
975
+
1182
976
  const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
1183
977
  const fromAreaPath = this.resolveMewpResponsibility(areaPath);
1184
978
  if (fromAreaPath) return fromAreaPath;
@@ -1225,6 +1019,8 @@ ORDER BY [System.Id]`;
1225
1019
  if (name) return String(name).trim();
1226
1020
  const uniqueName = (value as any).uniqueName;
1227
1021
  if (uniqueName) return String(uniqueName).trim();
1022
+ const objectValue = (value as any).value;
1023
+ if (objectValue !== undefined && objectValue !== null) return String(objectValue).trim();
1228
1024
  }
1229
1025
  return String(value).trim();
1230
1026
  }
@@ -1075,101 +1075,6 @@ describe('ResultDataProvider', () => {
1075
1075
  });
1076
1076
 
1077
1077
  describe('getMewpL2CoverageFlatResults', () => {
1078
- it('should support query-mode requirement scope for MEWP coverage', async () => {
1079
- jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1080
- jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1081
- jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
1082
- {
1083
- testPointsItems: [{ testCaseId: 101, lastRunId: 10, lastResultId: 20, testCaseName: 'TC 101' }],
1084
- testCasesItems: [
1085
- {
1086
- workItem: {
1087
- id: 101,
1088
- workItemFields: [{ key: 'System.Title', value: 'TC 101' }],
1089
- },
1090
- },
1091
- ],
1092
- },
1093
- ]);
1094
- jest.spyOn(resultDataProvider as any, 'fetchMewpRequirementTypeNames').mockResolvedValueOnce([
1095
- 'Requirement',
1096
- ]);
1097
- jest.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds').mockResolvedValueOnce([
1098
- {
1099
- id: 9001,
1100
- fields: {
1101
- 'System.WorkItemType': 'Requirement',
1102
- 'System.Title': 'Requirement from query',
1103
- 'Custom.CustomerId': 'SR3001',
1104
- 'System.AreaPath': 'MEWP\\IL',
1105
- },
1106
- relations: [
1107
- {
1108
- rel: 'Microsoft.VSTS.Common.TestedBy-Forward',
1109
- url: 'https://dev.azure.com/org/_apis/wit/workItems/101',
1110
- },
1111
- ],
1112
- },
1113
- ]);
1114
- jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
1115
- {
1116
- testCaseId: 101,
1117
- testCase: { id: 101, name: 'TC 101' },
1118
- iteration: {
1119
- actionResults: [{ action: 'Validate SR3001', expected: '', outcome: 'Passed' }],
1120
- },
1121
- },
1122
- ]);
1123
-
1124
- const TicketsProviderMock: any = require('../../modules/TicketsDataProvider').default;
1125
- TicketsProviderMock.mockImplementationOnce(() => ({
1126
- GetQueryResultsFromWiql: jest.fn().mockResolvedValue({
1127
- fetchedWorkItems: [
1128
- {
1129
- id: 9001,
1130
- fields: {
1131
- 'System.WorkItemType': 'Requirement',
1132
- 'System.Title': 'Requirement from query',
1133
- 'Custom.CustomerId': 'SR3001',
1134
- 'System.AreaPath': 'MEWP\\IL',
1135
- },
1136
- },
1137
- ],
1138
- }),
1139
- }));
1140
-
1141
- const result = await (resultDataProvider as any).getMewpL2CoverageFlatResults(
1142
- '123',
1143
- mockProjectName,
1144
- [1],
1145
- {
1146
- linkedQueryMode: 'query',
1147
- testAssociatedQuery: { wiql: { href: 'https://example.com/wiql' } },
1148
- }
1149
- );
1150
-
1151
- const row = result.rows.find((item: any) => item['Customer ID'] === 'SR3001');
1152
- expect(row).toEqual(
1153
- expect.objectContaining({
1154
- 'Title (Customer name)': 'Requirement from query',
1155
- 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
1156
- 'Test case id': 101,
1157
- 'Test case title': 'TC 101',
1158
- 'Number of passed steps': 1,
1159
- 'Number of failed steps': 0,
1160
- 'Number of not run tests': 0,
1161
- })
1162
- );
1163
-
1164
- expect(TicketsProviderMock).toHaveBeenCalled();
1165
- const instance = TicketsProviderMock.mock.results[0].value;
1166
- expect(instance.GetQueryResultsFromWiql).toHaveBeenCalledWith(
1167
- 'https://example.com/wiql',
1168
- true,
1169
- expect.any(Map)
1170
- );
1171
- });
1172
-
1173
1078
  it('should map SR ids from steps and output requirement-test-case coverage rows', async () => {
1174
1079
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1175
1080
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -1387,7 +1292,16 @@ describe('ResultDataProvider', () => {
1387
1292
  4321
1388
1293
  );
1389
1294
 
1390
- expect(requirementId).toBe('4321');
1295
+ expect(requirementId).toBe('SR4321');
1296
+ });
1297
+
1298
+ it('should derive responsibility from Custom.SAPWBS when present', () => {
1299
+ const responsibility = (resultDataProvider as any).deriveMewpResponsibility({
1300
+ 'Custom.SAPWBS': 'IL',
1301
+ 'System.AreaPath': 'MEWP\\ESUK',
1302
+ });
1303
+
1304
+ expect(responsibility).toBe('IL');
1391
1305
  });
1392
1306
  });
1393
1307