@elisra-devops/docgen-data-provider 1.89.0 → 1.91.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.89.0",
3
+ "version": "1.91.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,11 +15,6 @@ export interface MewpExternalFileRef {
15
15
  export interface MewpCoverageRequestOptions {
16
16
  externalBugsFile?: MewpExternalFileRef | null;
17
17
  externalL3L4File?: MewpExternalFileRef | null;
18
- debugMode?: boolean;
19
- }
20
-
21
- export interface MewpInternalValidationRequestOptions {
22
- debugMode?: boolean;
23
18
  }
24
19
 
25
20
  export interface MewpRequirementStepSummary {
@@ -13,7 +13,6 @@ import type {
13
13
  MewpExternalTableValidationResult,
14
14
  MewpCoverageRow,
15
15
  MewpInternalValidationFlatPayload,
16
- MewpInternalValidationRequestOptions,
17
16
  MewpInternalValidationRow,
18
17
  MewpL2RequirementFamily,
19
18
  MewpL2RequirementWorkItem,
@@ -548,9 +547,6 @@ export default class ResultDataProvider {
548
547
  const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
549
548
  const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
550
549
  const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
551
- if (options?.debugMode) {
552
- this.logMewpRunScenarioDebugMatrix(runResults, `coverage plan=${testPlanId}`);
553
- }
554
550
  for (const runResult of runResults) {
555
551
  const testCaseId = this.extractMewpTestCaseId(runResult);
556
552
  const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
@@ -736,8 +732,7 @@ export default class ResultDataProvider {
736
732
  testPlanId: string,
737
733
  projectName: string,
738
734
  selectedSuiteIds: number[] | undefined,
739
- linkedQueryRequest?: any,
740
- options?: MewpInternalValidationRequestOptions
735
+ linkedQueryRequest?: any
741
736
  ): Promise<MewpInternalValidationFlatPayload> {
742
737
  const defaultPayload: MewpInternalValidationFlatPayload = {
743
738
  sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
@@ -767,6 +762,37 @@ export default class ResultDataProvider {
767
762
  allRequirements,
768
763
  scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
769
764
  );
765
+ const linkedFullCodesByTestCase = new Map<number, Set<string>>();
766
+ const linkedFamilyCodesAcrossTestCases = new Map<string, Set<string>>();
767
+ const linkedFullCodesAcrossTestCases = new Set<string>();
768
+ const linkedBaseKeysAcrossTestCases = new Set<string>();
769
+
770
+ for (const [linkedTestCaseId, links] of linkedRequirementsByTestCase.entries()) {
771
+ const rawFullCodes = links?.fullCodes || new Set<string>();
772
+ const filteredFullCodes =
773
+ scopedRequirementKeys?.size && rawFullCodes.size > 0
774
+ ? new Set<string>(
775
+ [...rawFullCodes].filter((code) =>
776
+ scopedRequirementKeys.has(this.toRequirementKey(code))
777
+ )
778
+ )
779
+ : rawFullCodes;
780
+ linkedFullCodesByTestCase.set(linkedTestCaseId, filteredFullCodes);
781
+
782
+ for (const code of filteredFullCodes) {
783
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(code);
784
+ if (!normalizedCode) continue;
785
+ linkedFullCodesAcrossTestCases.add(normalizedCode);
786
+
787
+ const baseKey = this.toRequirementKey(normalizedCode);
788
+ if (!baseKey) continue;
789
+ linkedBaseKeysAcrossTestCases.add(baseKey);
790
+ if (!linkedFamilyCodesAcrossTestCases.has(baseKey)) {
791
+ linkedFamilyCodesAcrossTestCases.set(baseKey, new Set<string>());
792
+ }
793
+ linkedFamilyCodesAcrossTestCases.get(baseKey)!.add(normalizedCode);
794
+ }
795
+ }
770
796
 
771
797
  const rows: MewpInternalValidationRow[] = [];
772
798
  const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
@@ -796,16 +822,6 @@ export default class ResultDataProvider {
796
822
  `fromSuitePayload=${preloadedStepXmlCount} fromWorkItemFallback=${fallbackStepLoadStats.loadedFromFallback} ` +
797
823
  `stepsXmlAvailable=${stepsXmlByTestCase.size} unresolved=${fallbackStepLoadStats.unresolvedCount}`
798
824
  );
799
- if (options?.debugMode) {
800
- const debugRunResults = await this.fetchAllResultDataTestReporter(
801
- testData,
802
- projectName,
803
- [],
804
- false,
805
- false
806
- );
807
- this.logMewpRunScenarioDebugMatrix(debugRunResults, `internal-validation plan=${testPlanId}`);
808
- }
809
825
 
810
826
  const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
811
827
  const diagnostics = {
@@ -859,18 +875,7 @@ export default class ResultDataProvider {
859
875
  [...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
860
876
  );
861
877
 
862
- const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
863
- const linkedFullCodes =
864
- scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
865
- ? new Set<string>(
866
- [...linkedFullCodesRaw].filter((code) =>
867
- scopedRequirementKeys.has(this.toRequirementKey(code))
868
- )
869
- )
870
- : linkedFullCodesRaw;
871
- const linkedBaseKeys = new Set<string>(
872
- [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
873
- );
878
+ const linkedFullCodes = linkedFullCodesByTestCase.get(testCaseId) || new Set<string>();
874
879
 
875
880
  const mentionedCodesByBase = new Map<string, Set<string>>();
876
881
  for (const code of mentionedL2Only) {
@@ -880,13 +885,12 @@ export default class ResultDataProvider {
880
885
  mentionedCodesByBase.get(baseKey)!.add(code);
881
886
  }
882
887
 
883
- // Context-based direction A logic:
884
- // 1) If no member of family is linked -> report only base SR (Step X: SR0054).
885
- // 2) If family is partially linked -> report only specific missing members.
886
- // 3) If family fully linked -> report nothing for that family.
888
+ // Direction A logic:
889
+ // 1) Base mention ("SR0054") is parent-level only and considered covered
890
+ // if any member of that family is linked across scoped test cases.
891
+ // 2) Child mention ("SR0054-1") is exact-match and checked across scoped test cases.
887
892
  const missingBaseWhenFamilyUncovered = new Set<string>();
888
893
  const missingSpecificMentionedNoFamily = new Set<string>();
889
- const missingFamilyMembers = new Set<string>();
890
894
  for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
891
895
  const familyCodes = requirementFamilies.get(baseKey);
892
896
  const mentionedCodesList = [...mentionedCodes];
@@ -894,30 +898,18 @@ export default class ResultDataProvider {
894
898
  const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
895
899
 
896
900
  if (familyCodes?.size) {
897
- // Base mention ("SR0054") validates against child coverage when children exist.
898
- // If no child variants exist, fallback to the single standalone requirement code.
901
+ const familyLinkedCodes = linkedFamilyCodesAcrossTestCases.get(baseKey) || new Set<string>();
902
+
903
+ // Base mention ("SR0054") is satisfied by any linked member in the same family.
899
904
  if (hasBaseMention) {
900
- const familyCodesList = [...familyCodes];
901
- const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
902
- const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
903
- const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
904
-
905
- if (missingInTargetFamily.length > 0) {
906
- const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
907
- if (!hasAnyLinkedInFamily) {
908
- missingBaseWhenFamilyUncovered.add(baseKey);
909
- } else {
910
- for (const code of missingInTargetFamily) {
911
- missingFamilyMembers.add(code);
912
- }
913
- }
905
+ if (familyLinkedCodes.size === 0) {
906
+ missingBaseWhenFamilyUncovered.add(baseKey);
914
907
  }
915
- continue;
916
908
  }
917
909
 
918
- // Specific mention ("SR0054-1") validates as exact-match only.
910
+ // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
919
911
  for (const code of mentionedSpecificMembers) {
920
- if (!linkedFullCodes.has(code)) {
912
+ if (!familyLinkedCodes.has(code)) {
921
913
  missingSpecificMentionedNoFamily.add(code);
922
914
  }
923
915
  }
@@ -928,10 +920,10 @@ export default class ResultDataProvider {
928
920
  for (const code of mentionedCodes) {
929
921
  const hasSpecificSuffix = /-\d+$/.test(code);
930
922
  if (hasSpecificSuffix) {
931
- if (!linkedFullCodes.has(code)) {
923
+ if (!linkedFullCodesAcrossTestCases.has(code)) {
932
924
  missingSpecificMentionedNoFamily.add(code);
933
925
  }
934
- } else if (!linkedBaseKeys.has(baseKey)) {
926
+ } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
935
927
  missingBaseWhenFamilyUncovered.add(baseKey);
936
928
  }
937
929
  }
@@ -961,7 +953,6 @@ export default class ResultDataProvider {
961
953
  const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
962
954
  a.localeCompare(b)
963
955
  );
964
- const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
965
956
  for (const code of sortedMissingSpecificMentionedNoFamily) {
966
957
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
967
958
  appendMentionedButNotLinked(code, stepRef);
@@ -970,11 +961,6 @@ export default class ResultDataProvider {
970
961
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
971
962
  appendMentionedButNotLinked(baseKey, stepRef);
972
963
  }
973
- for (const code of sortedMissingFamilyMembers) {
974
- const baseKey = this.toRequirementKey(code);
975
- const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
976
- appendMentionedButNotLinked(code, stepRef);
977
- }
978
964
 
979
965
  const sortedExtraLinked = [...new Set(extraLinked)]
980
966
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
@@ -994,11 +980,11 @@ export default class ResultDataProvider {
994
980
  return String(a[0]).localeCompare(String(b[0]));
995
981
  })
996
982
  .map(([stepRef, requirementIds]) => {
997
- const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
998
- return `${stepRef}: ${groupedRequirementList}`;
983
+ return this.formatStepScopedRequirementGroups(stepRef, requirementIds);
999
984
  })
1000
- .join('\n');
1001
- const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
985
+ .join('\n')
986
+ .trim();
987
+ const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked).trim();
1002
988
  const validationStatus: 'Pass' | 'Fail' =
1003
989
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1004
990
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -1008,8 +994,7 @@ export default class ResultDataProvider {
1008
994
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
1009
995
  `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
1010
996
  sortedMissingSpecificMentionedNoFamily.length +
1011
- sortedMissingBaseWhenFamilyUncovered.length +
1012
- sortedMissingFamilyMembers.length
997
+ sortedMissingBaseWhenFamilyUncovered.length
1013
998
  } ` +
1014
999
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
1015
1000
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
@@ -2630,6 +2615,49 @@ export default class ResultDataProvider {
2630
2615
  return `SR${match[1]}`;
2631
2616
  }
2632
2617
 
2618
+ private compareMewpRequirementCodes(a: string, b: string): number {
2619
+ const normalizeComparableCode = (value: string): { base: number; hasSuffix: number; suffix: number; raw: string } => {
2620
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(value);
2621
+ const match = /^SR(\d+)(?:-(\d+))?$/i.exec(normalizedCode);
2622
+ if (!match) {
2623
+ return {
2624
+ base: Number.POSITIVE_INFINITY,
2625
+ hasSuffix: 1,
2626
+ suffix: Number.POSITIVE_INFINITY,
2627
+ raw: String(value || ''),
2628
+ };
2629
+ }
2630
+
2631
+ return {
2632
+ base: Number(match[1]),
2633
+ hasSuffix: match[2] ? 1 : 0,
2634
+ suffix: match[2] ? Number(match[2]) : -1,
2635
+ raw: normalizedCode,
2636
+ };
2637
+ };
2638
+
2639
+ const left = normalizeComparableCode(a);
2640
+ const right = normalizeComparableCode(b);
2641
+
2642
+ if (left.base !== right.base) return left.base - right.base;
2643
+ if (left.hasSuffix !== right.hasSuffix) return left.hasSuffix - right.hasSuffix;
2644
+ if (left.suffix !== right.suffix) return left.suffix - right.suffix;
2645
+ return left.raw.localeCompare(right.raw);
2646
+ }
2647
+
2648
+ private formatStepScopedRequirementGroups(stepRef: string, requirementIds: Iterable<string>): string {
2649
+ const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
2650
+ if (!groupedRequirementList) return `${stepRef}:`;
2651
+
2652
+ const groupedLines = groupedRequirementList
2653
+ .split('\n')
2654
+ .map((line) => String(line || '').trim())
2655
+ .filter((line) => line.length > 0);
2656
+
2657
+ if (groupedLines.length <= 1) return `${stepRef}: ${groupedLines[0] || ''}`.trim();
2658
+ return `${stepRef}:\n${groupedLines.map((line) => `- ${line}`).join('\n')}`.trim();
2659
+ }
2660
+
2633
2661
  private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2634
2662
  const byBaseKey = new Map<string, Set<string>>();
2635
2663
  for (const rawCode of codes || []) {
@@ -2643,11 +2671,17 @@ export default class ResultDataProvider {
2643
2671
  if (byBaseKey.size === 0) return '';
2644
2672
 
2645
2673
  return [...byBaseKey.entries()]
2646
- .sort((a, b) => a[0].localeCompare(b[0]))
2674
+ .sort((a, b) => this.compareMewpRequirementCodes(a[0], b[0]))
2647
2675
  .map(([baseKey, members]) => {
2648
- const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2676
+ const sortedMembers = [...members].sort((a, b) => this.compareMewpRequirementCodes(a, b));
2649
2677
  if (sortedMembers.length <= 1) return sortedMembers[0];
2650
- return `${baseKey}: ${sortedMembers.join(', ')}`;
2678
+
2679
+ const nonBaseMembers = sortedMembers.filter((member) => member !== baseKey);
2680
+ if (nonBaseMembers.length > 0) {
2681
+ return `${baseKey}: ${nonBaseMembers.join(', ')}`;
2682
+ }
2683
+
2684
+ return baseKey;
2651
2685
  })
2652
2686
  .join('\n');
2653
2687
  }
@@ -3873,79 +3907,6 @@ export default class ResultDataProvider {
3873
3907
  return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId);
3874
3908
  }
3875
3909
 
3876
- private logMewpRunScenarioDebugMatrix(runResults: any[], contextLabel: string): void {
3877
- const results = Array.isArray(runResults) ? runResults : [];
3878
- const matrix = {
3879
- total: results.length,
3880
- passOrFailWithActionResults: 0,
3881
- runWithNoActionResults: 0,
3882
- notApplicable: 0,
3883
- noRunHistoryActive: 0,
3884
- other: 0,
3885
- };
3886
- const samples = {
3887
- passOrFailWithActionResults: [] as number[],
3888
- runWithNoActionResults: [] as number[],
3889
- notApplicable: [] as number[],
3890
- noRunHistoryActive: [] as number[],
3891
- other: [] as number[],
3892
- };
3893
-
3894
- const pushSample = (bucket: keyof typeof samples, id: number) => {
3895
- if (!Number.isFinite(id) || id <= 0) return;
3896
- if (samples[bucket].length >= 5) return;
3897
- samples[bucket].push(id);
3898
- };
3899
-
3900
- for (const item of results) {
3901
- const testCaseId = Number(item?.testCaseId || item?.testCase?.id || 0);
3902
- const hasRun = Number(item?.lastRunId || 0) > 0 && Number(item?.lastResultId || 0) > 0;
3903
- const rawOutcome = String(item?._debugTestOutcome || '').trim().toLowerCase();
3904
- const rawState = String(item?._debugTestCaseState || '').trim().toLowerCase();
3905
- const originalActionResultsCount = Number(item?._debugOriginalActionResultsCount ?? -1);
3906
-
3907
- if (rawOutcome === 'notapplicable' || rawOutcome === 'not applicable') {
3908
- matrix.notApplicable += 1;
3909
- pushSample('notApplicable', testCaseId);
3910
- continue;
3911
- }
3912
-
3913
- if (hasRun && (rawOutcome === 'passed' || rawOutcome === 'failed') && originalActionResultsCount > 0) {
3914
- matrix.passOrFailWithActionResults += 1;
3915
- pushSample('passOrFailWithActionResults', testCaseId);
3916
- continue;
3917
- }
3918
-
3919
- if (hasRun && originalActionResultsCount === 0) {
3920
- matrix.runWithNoActionResults += 1;
3921
- pushSample('runWithNoActionResults', testCaseId);
3922
- continue;
3923
- }
3924
-
3925
- if (!hasRun && rawState === 'active') {
3926
- matrix.noRunHistoryActive += 1;
3927
- pushSample('noRunHistoryActive', testCaseId);
3928
- continue;
3929
- }
3930
-
3931
- matrix.other += 1;
3932
- pushSample('other', testCaseId);
3933
- }
3934
-
3935
- logger.info(
3936
- `MEWP run debug matrix (${contextLabel}): total=${matrix.total}; ` +
3937
- `passOrFailWithActionResults=${matrix.passOrFailWithActionResults}; ` +
3938
- `runWithNoActionResults=${matrix.runWithNoActionResults}; ` +
3939
- `notApplicable=${matrix.notApplicable}; ` +
3940
- `noRunHistoryActive=${matrix.noRunHistoryActive}; other=${matrix.other}; ` +
3941
- `samplePassFail=${samples.passOrFailWithActionResults.join(',') || '-'}; ` +
3942
- `sampleNoAction=${samples.runWithNoActionResults.join(',') || '-'}; ` +
3943
- `sampleNA=${samples.notApplicable.join(',') || '-'}; ` +
3944
- `sampleNoRunActive=${samples.noRunHistoryActive.join(',') || '-'}; ` +
3945
- `sampleOther=${samples.other.join(',') || '-'}`
3946
- );
3947
- }
3948
-
3949
3910
  /**
3950
3911
  * Converts a run status string into a human-readable format.
3951
3912
  *
@@ -4381,11 +4342,6 @@ export default class ResultDataProvider {
4381
4342
  resultData.iterationDetails.push(iteration);
4382
4343
  }
4383
4344
 
4384
- const originalActionResultsCount = Array.isArray(iteration?.actionResults)
4385
- ? iteration.actionResults.length
4386
- : 0;
4387
- resultData._debugOriginalActionResultsCount = originalActionResultsCount;
4388
-
4389
4345
  if (resultData.stepsResultXml && iteration) {
4390
4346
  const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
4391
4347
  const actionResultsWithSharedModels = actionResults.filter(
@@ -5098,7 +5054,7 @@ export default class ResultDataProvider {
5098
5054
  resultData.iterationDetails?.length > 0
5099
5055
  ? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
5100
5056
  : undefined;
5101
- const debugOutcome = this.getTestOutcome(resultData);
5057
+ const testOutcome = this.getTestOutcome(resultData);
5102
5058
 
5103
5059
  if (!resultData?.testCase || !resultData?.testSuite) {
5104
5060
  logger.debug(
@@ -5132,9 +5088,6 @@ export default class ResultDataProvider {
5132
5088
  relatedCRs: resultData.relatedCRs || undefined,
5133
5089
  lastRunResult: undefined as any,
5134
5090
  customFields: {}, // Create an object to store custom fields
5135
- _debugTestOutcome: debugOutcome,
5136
- _debugTestCaseState: String(resultData?.state || ''),
5137
- _debugOriginalActionResultsCount: Number(resultData?._debugOriginalActionResultsCount ?? -1),
5138
5091
  };
5139
5092
 
5140
5093
  // Process all custom fields from resultData.filteredFields
@@ -5156,12 +5109,12 @@ export default class ResultDataProvider {
5156
5109
  case 'testCaseResult':
5157
5110
  if (lastRunId === undefined || lastResultId === undefined) {
5158
5111
  resultDataResponse.testCaseResult = {
5159
- resultMessage: `${this.convertRunStatus(debugOutcome)}`,
5112
+ resultMessage: `${this.convertRunStatus(testOutcome)}`,
5160
5113
  url: '',
5161
5114
  };
5162
5115
  } else {
5163
5116
  resultDataResponse.testCaseResult = {
5164
- resultMessage: `${this.convertRunStatus(debugOutcome)} in Run ${lastRunId}`,
5117
+ resultMessage: `${this.convertRunStatus(testOutcome)} in Run ${lastRunId}`,
5165
5118
  url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
5166
5119
  };
5167
5120
  }
@@ -1937,7 +1937,7 @@ describe('ResultDataProvider', () => {
1937
1937
  );
1938
1938
  });
1939
1939
 
1940
- it('should emit Direction A rows with step context for missing sibling links', async () => {
1940
+ it('should emit Direction A rows only for specifically mentioned child requirements', async () => {
1941
1941
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1942
1942
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1943
1943
  jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
@@ -1981,7 +1981,7 @@ describe('ResultDataProvider', () => {
1981
1981
  stepId: '2',
1982
1982
  stepPosition: '2',
1983
1983
  action: '',
1984
- expected: 'SR0001; SR0002',
1984
+ expected: 'SR0001-1',
1985
1985
  isSharedStepTitle: false,
1986
1986
  },
1987
1987
  ]);
@@ -2077,6 +2077,115 @@ describe('ResultDataProvider', () => {
2077
2077
  );
2078
2078
  });
2079
2079
 
2080
+ it('should support cross-test-case family coverage when siblings are linked on different test cases', async () => {
2081
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2082
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2083
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2084
+ {
2085
+ testPointsItems: [
2086
+ { testCaseId: 501, testCaseName: 'TC 501 - sibling 1' },
2087
+ { testCaseId: 502, testCaseName: 'TC 502 - sibling 2' },
2088
+ ],
2089
+ testCasesItems: [
2090
+ {
2091
+ workItem: {
2092
+ id: 501,
2093
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-501\"></steps>' }],
2094
+ },
2095
+ },
2096
+ {
2097
+ workItem: {
2098
+ id: 502,
2099
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-502\"></steps>' }],
2100
+ },
2101
+ },
2102
+ ],
2103
+ },
2104
+ ]);
2105
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2106
+ {
2107
+ workItemId: 9301,
2108
+ requirementId: 'SR0054-1',
2109
+ baseKey: 'SR0054',
2110
+ title: 'SR0054 child 1',
2111
+ responsibility: 'ESUK',
2112
+ linkedTestCaseIds: [],
2113
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2114
+ },
2115
+ {
2116
+ workItemId: 9302,
2117
+ requirementId: 'SR0054-2',
2118
+ baseKey: 'SR0054',
2119
+ title: 'SR0054 child 2',
2120
+ responsibility: 'ESUK',
2121
+ linkedTestCaseIds: [],
2122
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2123
+ },
2124
+ ]);
2125
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2126
+ new Map([
2127
+ [
2128
+ 501,
2129
+ {
2130
+ baseKeys: new Set(['SR0054']),
2131
+ fullCodes: new Set(['SR0054-1']),
2132
+ },
2133
+ ],
2134
+ [
2135
+ 502,
2136
+ {
2137
+ baseKeys: new Set(['SR0054']),
2138
+ fullCodes: new Set(['SR0054-2']),
2139
+ },
2140
+ ],
2141
+ ])
2142
+ );
2143
+ jest
2144
+ .spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps')
2145
+ .mockResolvedValueOnce([
2146
+ {
2147
+ stepId: '1',
2148
+ stepPosition: '1',
2149
+ action: 'Parent mention on first test case',
2150
+ expected: 'SR0054',
2151
+ isSharedStepTitle: false,
2152
+ },
2153
+ ])
2154
+ .mockResolvedValueOnce([
2155
+ {
2156
+ stepId: '1',
2157
+ stepPosition: '1',
2158
+ action: 'Parent mention on second test case',
2159
+ expected: 'SR0054',
2160
+ isSharedStepTitle: false,
2161
+ },
2162
+ ]);
2163
+
2164
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2165
+ '123',
2166
+ mockProjectName,
2167
+ [1]
2168
+ );
2169
+
2170
+ const byTestCase = new Map<number, any>(
2171
+ result.rows.map((row: any) => [Number(row['Test Case ID']), row])
2172
+ );
2173
+ expect(byTestCase.get(501)).toEqual(
2174
+ expect.objectContaining({
2175
+ 'Mentioned but Not Linked': '',
2176
+ 'Linked but Not Mentioned': '',
2177
+ 'Validation Status': 'Pass',
2178
+ })
2179
+ );
2180
+ expect(byTestCase.get(502)).toEqual(
2181
+ expect.objectContaining({
2182
+ 'Mentioned but Not Linked': '',
2183
+ 'Linked but Not Mentioned': '',
2184
+ 'Validation Status': 'Pass',
2185
+ })
2186
+ );
2187
+ });
2188
+
2080
2189
  it('should group linked-but-not-mentioned requirements by SR family', async () => {
2081
2190
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2082
2191
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2562,7 +2671,7 @@ describe('ResultDataProvider', () => {
2562
2671
  expect(byTestCase.get(201)).toEqual(
2563
2672
  expect.objectContaining({
2564
2673
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2565
- 'Mentioned but Not Linked': 'Step 1: SR0095-3\nSR0511: SR0511-1, SR0511-2',
2674
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3',
2566
2675
  'Linked but Not Mentioned': 'SR8888',
2567
2676
  'Validation Status': 'Fail',
2568
2677
  })