@elisra-devops/docgen-data-provider 1.92.0 → 1.94.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.92.0",
3
+ "version": "1.94.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,6 +52,11 @@ const pLimit = require('p-limit');
52
52
  * Instantiate the class with the organization URL and token, and use the provided methods to fetch and process test data.
53
53
  */
54
54
  export default class ResultDataProvider {
55
+ private static readonly MEWP_INTERNAL_VALIDATION_TRACE_TAG = '[MEWP][InternalValidation][Trace]';
56
+ private static readonly MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG =
57
+ '[MEWP][InternalValidation][Diagnostics]';
58
+ private static readonly MEWP_INTERNAL_VALIDATION_SUMMARY_TAG = '[MEWP][InternalValidation][Summary]';
59
+
55
60
  private static readonly MEWP_L2_COVERAGE_COLUMNS = [
56
61
  'L2 REQ ID',
57
62
  'L2 REQ Title',
@@ -732,7 +737,10 @@ export default class ResultDataProvider {
732
737
  testPlanId: string,
733
738
  projectName: string,
734
739
  selectedSuiteIds: number[] | undefined,
735
- linkedQueryRequest?: any
740
+ linkedQueryRequest?: any,
741
+ options?: {
742
+ debugMode?: boolean;
743
+ }
736
744
  ): Promise<MewpInternalValidationFlatPayload> {
737
745
  const defaultPayload: MewpInternalValidationFlatPayload = {
738
746
  sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
@@ -832,9 +840,19 @@ export default class ResultDataProvider {
832
840
  testCasesWithoutMentionedCustomerIds: 0,
833
841
  failingRows: 0,
834
842
  };
843
+ const traceInternalValidation = options?.debugMode === true;
844
+ if (traceInternalValidation) {
845
+ logger.info(
846
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
847
+ mode: 'enabled',
848
+ source: 'ui-debug-mode',
849
+ })
850
+ );
851
+ }
835
852
 
836
853
  for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
837
854
  diagnostics.totalTestCases += 1;
855
+ const traceCurrentTestCase = traceInternalValidation;
838
856
  const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
839
857
  const parsedSteps =
840
858
  stepsXml && String(stepsXml).trim() !== ''
@@ -884,6 +902,23 @@ export default class ResultDataProvider {
884
902
  if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
885
903
  mentionedCodesByBase.get(baseKey)!.add(code);
886
904
  }
905
+ if (traceCurrentTestCase) {
906
+ logger.debug(
907
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
908
+ event: 'test-case-start',
909
+ tc: testCaseId,
910
+ parsedSteps: executableSteps.length,
911
+ stepsWithMentions: mentionEntries.length,
912
+ mentionedCodes:
913
+ [...mentionedL2Only].sort((a, b) => this.compareMewpRequirementCodes(a, b)).join('; ') ||
914
+ '<none>',
915
+ linkedCodesInTestCase:
916
+ [...linkedFullCodes]
917
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
918
+ .join('; ') || '<none>',
919
+ })
920
+ );
921
+ }
887
922
 
888
923
  // Direction A logic:
889
924
  // 1) Base mention ("SR0054") is parent-level and considered covered only when
@@ -901,43 +936,110 @@ export default class ResultDataProvider {
901
936
 
902
937
  if (familyCodes?.size) {
903
938
  const familyLinkedCodes = linkedFamilyCodesAcrossTestCases.get(baseKey) || new Set<string>();
939
+ const normalizedFamilyMembers = [...familyCodes]
940
+ .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
941
+ .filter((code) => !!code);
942
+ const specificFamilyMembers = normalizedFamilyMembers.filter((code) => /-\d+$/.test(code));
943
+ const requiredFamilyMembers =
944
+ specificFamilyMembers.length > 0 ? specificFamilyMembers : normalizedFamilyMembers;
904
945
 
905
946
  // Base mention ("SR0054") requires full family coverage across selected test cases.
906
947
  if (hasBaseMention) {
907
- const normalizedFamilyMembers = [...familyCodes]
908
- .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
909
- .filter((code) => !!code);
910
- const specificFamilyMembers = normalizedFamilyMembers.filter((code) => /-\d+$/.test(code));
911
- const requiredFamilyMembers =
912
- specificFamilyMembers.length > 0 ? specificFamilyMembers : normalizedFamilyMembers;
948
+ const missingRequiredFamilyMembers = requiredFamilyMembers.filter(
949
+ (memberCode) => !familyLinkedCodes.has(memberCode)
950
+ );
913
951
  const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
914
952
  familyLinkedCodes.has(memberCode)
915
953
  );
916
954
  if (!isWholeFamilyCovered) {
917
955
  missingBaseWhenFamilyUncovered.add(baseKey);
918
956
  }
957
+ if (traceCurrentTestCase) {
958
+ logger.debug(
959
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
960
+ event: 'base-family-coverage',
961
+ tc: testCaseId,
962
+ base: baseKey,
963
+ baseMention: true,
964
+ requiredFamily:
965
+ requiredFamilyMembers
966
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
967
+ .join('; ') || '<none>',
968
+ linkedAcrossScope:
969
+ [...familyLinkedCodes]
970
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
971
+ .join('; ') || '<none>',
972
+ missingRequired:
973
+ missingRequiredFamilyMembers
974
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
975
+ .join('; ') || '<none>',
976
+ covered: isWholeFamilyCovered,
977
+ })
978
+ );
979
+ }
919
980
  }
920
981
 
921
982
  // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
922
- for (const code of mentionedSpecificMembers) {
923
- if (!familyLinkedCodes.has(code)) {
924
- missingSpecificMentionedNoFamily.add(code);
925
- }
983
+ const missingSpecificMembers = mentionedSpecificMembers.filter(
984
+ (code) => !familyLinkedCodes.has(code)
985
+ );
986
+ for (const code of missingSpecificMembers) {
987
+ missingSpecificMentionedNoFamily.add(code);
988
+ }
989
+ if (traceCurrentTestCase && mentionedSpecificMembers.length > 0) {
990
+ logger.debug(
991
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
992
+ event: 'specific-members-check',
993
+ tc: testCaseId,
994
+ base: baseKey,
995
+ specificMentioned:
996
+ mentionedSpecificMembers
997
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
998
+ .join('; ') || '<none>',
999
+ specificMissing:
1000
+ missingSpecificMembers
1001
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1002
+ .join('; ') || '<none>',
1003
+ })
1004
+ );
926
1005
  }
927
1006
  continue;
928
1007
  }
929
1008
 
930
1009
  // Fallback path when family data is unavailable for this base key.
1010
+ const fallbackMissingSpecific: string[] = [];
1011
+ let fallbackMissingBase = false;
931
1012
  for (const code of mentionedCodes) {
932
1013
  const hasSpecificSuffix = /-\d+$/.test(code);
933
1014
  if (hasSpecificSuffix) {
934
1015
  if (!linkedFullCodesAcrossTestCases.has(code)) {
935
1016
  missingSpecificMentionedNoFamily.add(code);
1017
+ fallbackMissingSpecific.push(code);
936
1018
  }
937
1019
  } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
938
1020
  missingBaseWhenFamilyUncovered.add(baseKey);
1021
+ fallbackMissingBase = true;
939
1022
  }
940
1023
  }
1024
+ if (traceCurrentTestCase) {
1025
+ logger.debug(
1026
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1027
+ event: 'fallback-path',
1028
+ tc: testCaseId,
1029
+ base: baseKey,
1030
+ fallbackUsed: true,
1031
+ mentioned:
1032
+ mentionedCodesList
1033
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1034
+ .join('; ') || '<none>',
1035
+ missingSpecific:
1036
+ fallbackMissingSpecific
1037
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1038
+ .join('; ') || '<none>',
1039
+ missingBase: fallbackMissingBase,
1040
+ })
1041
+ );
1042
+ }
941
1043
  }
942
1044
 
943
1045
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -983,6 +1085,16 @@ export default class ResultDataProvider {
983
1085
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
984
1086
  appendMentionedButNotLinked(baseKey, stepRef);
985
1087
  }
1088
+ if (traceCurrentTestCase) {
1089
+ logger.debug(
1090
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1091
+ event: 'direction-a-summary',
1092
+ tc: testCaseId,
1093
+ missingSpecific: sortedMissingSpecificMentionedNoFamily.join('; ') || '<none>',
1094
+ missingBase: sortedMissingBaseWhenFamilyUncovered.join('; ') || '<none>',
1095
+ })
1096
+ );
1097
+ }
986
1098
 
987
1099
  const sortedExtraLinked = [...new Set(extraLinked)]
988
1100
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
@@ -1022,15 +1134,19 @@ export default class ResultDataProvider {
1022
1134
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1023
1135
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
1024
1136
  logger.debug(
1025
- `MEWP internal validation parse diagnostics: ` +
1026
- `testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
1027
- `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
1028
- `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
1137
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1138
+ testCaseId,
1139
+ parsedSteps: executableSteps.length,
1140
+ stepsWithMentions: mentionEntries.length,
1141
+ customerIdsFound: mentionedL2Only.size,
1142
+ linkedRequirements: linkedFullCodes.size,
1143
+ mentionedButNotLinked:
1029
1144
  sortedMissingSpecificMentionedNoFamily.length +
1030
- sortedMissingBaseWhenFamilyUncovered.length
1031
- } ` +
1032
- `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
1033
- `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
1145
+ sortedMissingBaseWhenFamilyUncovered.length,
1146
+ linkedButNotMentioned: sortedExtraLinked.length,
1147
+ status: validationStatus,
1148
+ customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1149
+ })
1034
1150
  );
1035
1151
 
1036
1152
  rows.push({
@@ -1042,11 +1158,14 @@ export default class ResultDataProvider {
1042
1158
  });
1043
1159
  }
1044
1160
  logger.info(
1045
- `MEWP internal validation summary: testCases=${diagnostics.totalTestCases} ` +
1046
- `parsedSteps=${diagnostics.totalParsedSteps} stepsWithMentions=${diagnostics.totalStepsWithMentions} ` +
1047
- `totalCustomerIdsFound=${diagnostics.totalMentionedCustomerIds} ` +
1048
- `testCasesWithoutCustomerIds=${diagnostics.testCasesWithoutMentionedCustomerIds} ` +
1049
- `failingRows=${diagnostics.failingRows}`
1161
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_SUMMARY_TAG, {
1162
+ testCases: diagnostics.totalTestCases,
1163
+ parsedSteps: diagnostics.totalParsedSteps,
1164
+ stepsWithMentions: diagnostics.totalStepsWithMentions,
1165
+ totalCustomerIdsFound: diagnostics.totalMentionedCustomerIds,
1166
+ testCasesWithoutCustomerIds: diagnostics.testCasesWithoutMentionedCustomerIds,
1167
+ failingRows: diagnostics.failingRows,
1168
+ })
1050
1169
  );
1051
1170
 
1052
1171
  return {
@@ -1163,6 +1282,17 @@ export default class ResultDataProvider {
1163
1282
  return `MEWP Internal Validation - ${suffix}`;
1164
1283
  }
1165
1284
 
1285
+ private formatLogValue(value: any): string {
1286
+ if (value === null || value === undefined) return '<none>';
1287
+ const asText = String(value).trim();
1288
+ return asText !== '' ? asText : '<none>';
1289
+ }
1290
+
1291
+ private buildTaggedLogMessage(tag: string, fields: Record<string, any>): string {
1292
+ const sections = Object.entries(fields).map(([key, value]) => `${key}=${this.formatLogValue(value)}`);
1293
+ return `${tag} ${sections.join(' | ')}`;
1294
+ }
1295
+
1166
1296
  private createMewpCoverageRow(
1167
1297
  requirement: Pick<
1168
1298
  MewpL2RequirementFamily,
@@ -2698,7 +2828,7 @@ export default class ResultDataProvider {
2698
2828
  // Direction B display is family-level when multiple members exist.
2699
2829
  return baseKey;
2700
2830
  })
2701
- .join('\n');
2831
+ .join('; ');
2702
2832
  }
2703
2833
 
2704
2834
  private toMewpComparableText(value: any): string {
@@ -2919,7 +3049,7 @@ export default class ResultDataProvider {
2919
3049
  // Fetch detailed information for each test point and map to required format
2920
3050
  const detailedPoints = await Promise.all(
2921
3051
  latestPoints.map(async (point: any) => {
2922
- const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true`;
3052
+ const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps,System.Rev&includePointDetails=true`;
2923
3053
  const detailedPoint = await TFSServices.getItemContent(url, this.token);
2924
3054
  return this.mapTestPointForCrossPlans(detailedPoint, projectName);
2925
3055
  // return this.mapTestPointForCrossPlans(detailedPoint, projectName);
@@ -3012,7 +3142,7 @@ export default class ResultDataProvider {
3012
3142
  testPlanId: string,
3013
3143
  suiteId: string
3014
3144
  ): Promise<any[]> {
3015
- const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${suiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps`;
3145
+ const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${suiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps,System.Rev`;
3016
3146
 
3017
3147
  const { value: testCases } = await TFSServices.getItemContent(url, this.token);
3018
3148
 
@@ -3056,8 +3186,28 @@ export default class ResultDataProvider {
3056
3186
  return fields;
3057
3187
  }
3058
3188
 
3189
+ private getFieldValueByName(fields: Record<string, any>, fieldName: string): any {
3190
+ if (!fields || typeof fields !== 'object') return undefined;
3191
+ if (Object.prototype.hasOwnProperty.call(fields, fieldName)) {
3192
+ return fields[fieldName];
3193
+ }
3194
+
3195
+ const lookupName = String(fieldName || '').toLowerCase().trim();
3196
+ if (!lookupName) return undefined;
3197
+ const matchedKey = Object.keys(fields).find(
3198
+ (key) => String(key || '').toLowerCase().trim() === lookupName
3199
+ );
3200
+ return matchedKey ? fields[matchedKey] : undefined;
3201
+ }
3202
+
3059
3203
  private resolveSuiteTestCaseRevision(testCaseItem: any): number {
3204
+ const fieldsFromList = this.extractWorkItemFieldsMap(testCaseItem?.workItem?.workItemFields);
3205
+ const fieldsFromMap = testCaseItem?.workItem?.fields || {};
3206
+ const systemRevFromList = this.getFieldValueByName(fieldsFromList, 'System.Rev');
3207
+ const systemRevFromMap = this.getFieldValueByName(fieldsFromMap, 'System.Rev');
3060
3208
  const revisionCandidates = [
3209
+ systemRevFromList,
3210
+ systemRevFromMap,
3061
3211
  testCaseItem?.workItem?.rev,
3062
3212
  testCaseItem?.workItem?.revision,
3063
3213
  testCaseItem?.workItem?.version,
@@ -679,6 +679,34 @@ describe('ResultDataProvider', () => {
679
679
 
680
680
  // Assert
681
681
  expect(result).toEqual(mockTestCases.value);
682
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
683
+ expect.stringContaining(
684
+ `/_apis/testplan/Plans/${mockTestPlanId}/Suites/${mockSuiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps,System.Rev`
685
+ ),
686
+ mockToken
687
+ );
688
+ });
689
+ });
690
+
691
+ describe('resolveSuiteTestCaseRevision', () => {
692
+ it('should resolve System.Rev from workItemFields', () => {
693
+ const revision = (resultDataProvider as any).resolveSuiteTestCaseRevision({
694
+ workItem: {
695
+ workItemFields: [{ key: 'System.Rev', value: '12' }],
696
+ },
697
+ });
698
+
699
+ expect(revision).toBe(12);
700
+ });
701
+
702
+ it('should resolve System.Rev case-insensitively from workItem fields map', () => {
703
+ const revision = (resultDataProvider as any).resolveSuiteTestCaseRevision({
704
+ workItem: {
705
+ fields: { 'system.rev': 14 },
706
+ },
707
+ });
708
+
709
+ expect(revision).toBe(14);
682
710
  });
683
711
  });
684
712
 
@@ -837,7 +865,7 @@ describe('ResultDataProvider', () => {
837
865
  const result = await (resultDataProvider as any).fetchCrossTestPoints(mockProjectName, [1, 2]);
838
866
 
839
867
  expect(TFSServices.getItemContent).toHaveBeenCalledWith(
840
- 'https://example.com/points/2?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true',
868
+ 'https://example.com/points/2?witFields=Microsoft.VSTS.TCM.Steps,System.Rev&includePointDetails=true',
841
869
  mockToken
842
870
  );
843
871
  expect(result).toHaveLength(2);
@@ -2993,7 +3021,7 @@ describe('ResultDataProvider', () => {
2993
3021
  'Test Case ID': 42,
2994
3022
  'Test Case Title': 'TC-0042',
2995
3023
  'Mentioned but Not Linked': 'Step 3: SR0027',
2996
- 'Linked but Not Mentioned': 'SR0817\nSR0818\nSR0859',
3024
+ 'Linked but Not Mentioned': 'SR0817; SR0818; SR0859',
2997
3025
  'Validation Status': 'Fail',
2998
3026
  })
2999
3027
  );
@@ -3874,6 +3902,53 @@ describe('ResultDataProvider', () => {
3874
3902
  expect(res).toEqual(expect.objectContaining({ testCaseRevision: 9 }));
3875
3903
  });
3876
3904
 
3905
+ it('should resolve no-run revision from System.Rev in suite test-case fields', async () => {
3906
+ const point = {
3907
+ testCaseId: '321',
3908
+ testCaseName: 'TC 321',
3909
+ outcome: 'Not Run',
3910
+ suiteTestCase: {
3911
+ workItem: {
3912
+ id: 321,
3913
+ workItemFields: [
3914
+ { key: 'System.Rev', value: '13' },
3915
+ { key: 'Microsoft.VSTS.TCM.Steps', value: '<steps></steps>' },
3916
+ ],
3917
+ },
3918
+ },
3919
+ testSuite: { id: '1', name: 'Suite' },
3920
+ };
3921
+
3922
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
3923
+ id: 321,
3924
+ rev: 13,
3925
+ fields: {
3926
+ 'System.State': 'Design',
3927
+ 'System.CreatedDate': '2024-05-01T00:00:00',
3928
+ 'Microsoft.VSTS.TCM.Priority': 1,
3929
+ 'System.Title': 'Title 321',
3930
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
3931
+ },
3932
+ relations: [],
3933
+ });
3934
+
3935
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
3936
+ mockProjectName,
3937
+ '0',
3938
+ '0',
3939
+ true,
3940
+ [],
3941
+ false,
3942
+ point
3943
+ );
3944
+
3945
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
3946
+ expect.stringContaining('/_apis/wit/workItems/321/revisions/13?$expand=all'),
3947
+ mockToken
3948
+ );
3949
+ expect(res).toEqual(expect.objectContaining({ testCaseRevision: 13 }));
3950
+ });
3951
+
3877
3952
  it('should append linked relations and filter testCaseWorkItemField when isTestReporter=true and isQueryMode=false', async () => {
3878
3953
  (TFSServices.getItemContent as jest.Mock).mockReset();
3879
3954
 
@@ -5045,6 +5120,35 @@ describe('ResultDataProvider', () => {
5045
5120
  expect(fetchStrategy).not.toHaveBeenCalled();
5046
5121
  expect(result).toEqual([]);
5047
5122
  });
5123
+
5124
+ it('should keep points without run/result IDs when test reporter mode is enabled', async () => {
5125
+ const testData = [
5126
+ {
5127
+ testSuiteId: 1,
5128
+ testPointsItems: [{ testCaseId: 10, lastRunId: 101, lastResultId: 201 }, { testCaseId: 11 }],
5129
+ },
5130
+ ];
5131
+ const fetchStrategy = jest
5132
+ .fn()
5133
+ .mockResolvedValueOnce({ testCaseId: 10 })
5134
+ .mockResolvedValueOnce({ testCaseId: 11 });
5135
+
5136
+ const result = await (resultDataProvider as any).fetchAllResultDataBase(
5137
+ testData,
5138
+ mockProjectName,
5139
+ true,
5140
+ fetchStrategy
5141
+ );
5142
+
5143
+ expect(fetchStrategy).toHaveBeenCalledTimes(2);
5144
+ expect(fetchStrategy).toHaveBeenNthCalledWith(
5145
+ 2,
5146
+ mockProjectName,
5147
+ 1,
5148
+ expect.objectContaining({ testCaseId: 11 })
5149
+ );
5150
+ expect(result).toEqual([{ testCaseId: 10 }, { testCaseId: 11 }]);
5151
+ });
5048
5152
  });
5049
5153
 
5050
5154
  describe('fetchResultDataBase', () => {
@@ -5071,6 +5175,26 @@ describe('ResultDataProvider', () => {
5071
5175
  expect(fetchResultMethod).toHaveBeenCalled();
5072
5176
  expect(result).toBeDefined();
5073
5177
  });
5178
+
5179
+ it('should call fetch method with runId/resultId as 0 when point has no run history', async () => {
5180
+ const point = { testCaseId: 15, lastRunId: undefined, lastResultId: undefined };
5181
+ const fetchResultMethod = jest.fn().mockResolvedValue({
5182
+ testCase: { id: 15, name: 'TC 15' },
5183
+ testSuite: { name: 'S' },
5184
+ iterationDetails: [],
5185
+ });
5186
+ const createResponseObject = jest.fn().mockReturnValue({ id: 15 });
5187
+
5188
+ await (resultDataProvider as any).fetchResultDataBase(
5189
+ mockProjectName,
5190
+ 'suite-no-runs',
5191
+ point,
5192
+ fetchResultMethod,
5193
+ createResponseObject
5194
+ );
5195
+
5196
+ expect(fetchResultMethod).toHaveBeenCalledWith(mockProjectName, '0', '0');
5197
+ });
5074
5198
  });
5075
5199
 
5076
5200
  describe('getCombinedResultsSummary', () => {
@@ -5460,6 +5584,86 @@ describe('ResultDataProvider', () => {
5460
5584
  })
5461
5585
  );
5462
5586
  });
5587
+
5588
+ it('should return test-level row with empty step fields when suite has no run history', async () => {
5589
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan 12');
5590
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([
5591
+ {
5592
+ testSuiteId: 300,
5593
+ suiteId: 300,
5594
+ suiteName: 'suite no runs',
5595
+ parentSuiteId: 100,
5596
+ parentSuiteName: 'Rel3',
5597
+ suitePath: 'Root/Rel3/suite no runs',
5598
+ testGroupName: 'suite no runs',
5599
+ },
5600
+ ]);
5601
+
5602
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
5603
+ {
5604
+ testSuiteId: 300,
5605
+ suiteId: 300,
5606
+ suiteName: 'suite no runs',
5607
+ parentSuiteId: 100,
5608
+ parentSuiteName: 'Rel3',
5609
+ suitePath: 'Root/Rel3/suite no runs',
5610
+ testGroupName: 'suite no runs',
5611
+ testPointsItems: [
5612
+ {
5613
+ testCaseId: 55,
5614
+ testCaseName: 'TC 55',
5615
+ outcome: 'Not Run',
5616
+ testPointId: 9001,
5617
+ lastRunId: undefined,
5618
+ lastResultId: undefined,
5619
+ lastResultDetails: undefined,
5620
+ },
5621
+ ],
5622
+ testCasesItems: [
5623
+ {
5624
+ workItem: {
5625
+ id: 55,
5626
+ workItemFields: [{ key: 'System.Rev', value: 4 }],
5627
+ },
5628
+ },
5629
+ ],
5630
+ },
5631
+ ]);
5632
+
5633
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
5634
+ {
5635
+ testCaseId: 55,
5636
+ testCase: { id: 55, name: 'TC 55' },
5637
+ testSuite: { name: 'suite no runs' },
5638
+ executionDate: '',
5639
+ testCaseResult: { resultMessage: 'Not Run', url: '' },
5640
+ customFields: {},
5641
+ runBy: '',
5642
+ iteration: undefined,
5643
+ lastRunId: undefined,
5644
+ lastResultId: undefined,
5645
+ },
5646
+ ]);
5647
+
5648
+ const result = await resultDataProvider.getTestReporterFlatResults(
5649
+ mockTestPlanId,
5650
+ mockProjectName,
5651
+ undefined,
5652
+ [],
5653
+ false
5654
+ );
5655
+
5656
+ expect(result.rows).toHaveLength(1);
5657
+ expect(result.rows[0]).toEqual(
5658
+ expect.objectContaining({
5659
+ testCaseId: 55,
5660
+ testRunId: undefined,
5661
+ testPointId: 9001,
5662
+ stepOutcome: undefined,
5663
+ stepStepIdentifier: '',
5664
+ })
5665
+ );
5666
+ });
5463
5667
  });
5464
5668
 
5465
5669
  describe('getCombinedResultsSummary - appendix branches', () => {