@elisra-devops/docgen-data-provider 1.90.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.90.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",
@@ -762,6 +762,37 @@ export default class ResultDataProvider {
762
762
  allRequirements,
763
763
  scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
764
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
+ }
765
796
 
766
797
  const rows: MewpInternalValidationRow[] = [];
767
798
  const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
@@ -844,18 +875,7 @@ export default class ResultDataProvider {
844
875
  [...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
845
876
  );
846
877
 
847
- const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
848
- const linkedFullCodes =
849
- scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
850
- ? new Set<string>(
851
- [...linkedFullCodesRaw].filter((code) =>
852
- scopedRequirementKeys.has(this.toRequirementKey(code))
853
- )
854
- )
855
- : linkedFullCodesRaw;
856
- const linkedBaseKeys = new Set<string>(
857
- [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
858
- );
878
+ const linkedFullCodes = linkedFullCodesByTestCase.get(testCaseId) || new Set<string>();
859
879
 
860
880
  const mentionedCodesByBase = new Map<string, Set<string>>();
861
881
  for (const code of mentionedL2Only) {
@@ -865,13 +885,12 @@ export default class ResultDataProvider {
865
885
  mentionedCodesByBase.get(baseKey)!.add(code);
866
886
  }
867
887
 
868
- // Context-based direction A logic:
869
- // 1) If no member of family is linked -> report only base SR (Step X: SR0054).
870
- // 2) If family is partially linked -> report only specific missing members.
871
- // 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.
872
892
  const missingBaseWhenFamilyUncovered = new Set<string>();
873
893
  const missingSpecificMentionedNoFamily = new Set<string>();
874
- const missingFamilyMembers = new Set<string>();
875
894
  for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
876
895
  const familyCodes = requirementFamilies.get(baseKey);
877
896
  const mentionedCodesList = [...mentionedCodes];
@@ -879,30 +898,18 @@ export default class ResultDataProvider {
879
898
  const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
880
899
 
881
900
  if (familyCodes?.size) {
882
- // Base mention ("SR0054") validates against child coverage when children exist.
883
- // 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.
884
904
  if (hasBaseMention) {
885
- const familyCodesList = [...familyCodes];
886
- const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
887
- const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
888
- const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
889
-
890
- if (missingInTargetFamily.length > 0) {
891
- const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
892
- if (!hasAnyLinkedInFamily) {
893
- missingBaseWhenFamilyUncovered.add(baseKey);
894
- } else {
895
- for (const code of missingInTargetFamily) {
896
- missingFamilyMembers.add(code);
897
- }
898
- }
905
+ if (familyLinkedCodes.size === 0) {
906
+ missingBaseWhenFamilyUncovered.add(baseKey);
899
907
  }
900
- continue;
901
908
  }
902
909
 
903
- // Specific mention ("SR0054-1") validates as exact-match only.
910
+ // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
904
911
  for (const code of mentionedSpecificMembers) {
905
- if (!linkedFullCodes.has(code)) {
912
+ if (!familyLinkedCodes.has(code)) {
906
913
  missingSpecificMentionedNoFamily.add(code);
907
914
  }
908
915
  }
@@ -913,10 +920,10 @@ export default class ResultDataProvider {
913
920
  for (const code of mentionedCodes) {
914
921
  const hasSpecificSuffix = /-\d+$/.test(code);
915
922
  if (hasSpecificSuffix) {
916
- if (!linkedFullCodes.has(code)) {
923
+ if (!linkedFullCodesAcrossTestCases.has(code)) {
917
924
  missingSpecificMentionedNoFamily.add(code);
918
925
  }
919
- } else if (!linkedBaseKeys.has(baseKey)) {
926
+ } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
920
927
  missingBaseWhenFamilyUncovered.add(baseKey);
921
928
  }
922
929
  }
@@ -946,7 +953,6 @@ export default class ResultDataProvider {
946
953
  const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
947
954
  a.localeCompare(b)
948
955
  );
949
- const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
950
956
  for (const code of sortedMissingSpecificMentionedNoFamily) {
951
957
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
952
958
  appendMentionedButNotLinked(code, stepRef);
@@ -955,11 +961,6 @@ export default class ResultDataProvider {
955
961
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
956
962
  appendMentionedButNotLinked(baseKey, stepRef);
957
963
  }
958
- for (const code of sortedMissingFamilyMembers) {
959
- const baseKey = this.toRequirementKey(code);
960
- const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
961
- appendMentionedButNotLinked(code, stepRef);
962
- }
963
964
 
964
965
  const sortedExtraLinked = [...new Set(extraLinked)]
965
966
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
@@ -979,11 +980,11 @@ export default class ResultDataProvider {
979
980
  return String(a[0]).localeCompare(String(b[0]));
980
981
  })
981
982
  .map(([stepRef, requirementIds]) => {
982
- const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
983
- return `${stepRef}: ${groupedRequirementList}`;
983
+ return this.formatStepScopedRequirementGroups(stepRef, requirementIds);
984
984
  })
985
- .join('\n');
986
- const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
985
+ .join('\n')
986
+ .trim();
987
+ const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked).trim();
987
988
  const validationStatus: 'Pass' | 'Fail' =
988
989
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
989
990
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -993,8 +994,7 @@ export default class ResultDataProvider {
993
994
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
994
995
  `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
995
996
  sortedMissingSpecificMentionedNoFamily.length +
996
- sortedMissingBaseWhenFamilyUncovered.length +
997
- sortedMissingFamilyMembers.length
997
+ sortedMissingBaseWhenFamilyUncovered.length
998
998
  } ` +
999
999
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
1000
1000
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
@@ -2615,6 +2615,49 @@ export default class ResultDataProvider {
2615
2615
  return `SR${match[1]}`;
2616
2616
  }
2617
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
+
2618
2661
  private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2619
2662
  const byBaseKey = new Map<string, Set<string>>();
2620
2663
  for (const rawCode of codes || []) {
@@ -2628,11 +2671,17 @@ export default class ResultDataProvider {
2628
2671
  if (byBaseKey.size === 0) return '';
2629
2672
 
2630
2673
  return [...byBaseKey.entries()]
2631
- .sort((a, b) => a[0].localeCompare(b[0]))
2674
+ .sort((a, b) => this.compareMewpRequirementCodes(a[0], b[0]))
2632
2675
  .map(([baseKey, members]) => {
2633
- const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2676
+ const sortedMembers = [...members].sort((a, b) => this.compareMewpRequirementCodes(a, b));
2634
2677
  if (sortedMembers.length <= 1) return sortedMembers[0];
2635
- 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;
2636
2685
  })
2637
2686
  .join('\n');
2638
2687
  }
@@ -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
  })