@elisra-devops/docgen-data-provider 1.90.0 → 1.92.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.92.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,14 @@ 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 and considered covered only when
890
+ // the whole family is covered across scoped test cases:
891
+ // - if family has children, all children must be linked;
892
+ // - if family has no children, the base item itself must be linked.
893
+ // 2) Child mention ("SR0054-1") is exact-match and checked across scoped test cases.
872
894
  const missingBaseWhenFamilyUncovered = new Set<string>();
873
895
  const missingSpecificMentionedNoFamily = new Set<string>();
874
- const missingFamilyMembers = new Set<string>();
875
896
  for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
876
897
  const familyCodes = requirementFamilies.get(baseKey);
877
898
  const mentionedCodesList = [...mentionedCodes];
@@ -879,30 +900,27 @@ export default class ResultDataProvider {
879
900
  const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
880
901
 
881
902
  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.
903
+ const familyLinkedCodes = linkedFamilyCodesAcrossTestCases.get(baseKey) || new Set<string>();
904
+
905
+ // Base mention ("SR0054") requires full family coverage across selected test cases.
884
906
  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
- }
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;
913
+ const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
914
+ familyLinkedCodes.has(memberCode)
915
+ );
916
+ if (!isWholeFamilyCovered) {
917
+ missingBaseWhenFamilyUncovered.add(baseKey);
899
918
  }
900
- continue;
901
919
  }
902
920
 
903
- // Specific mention ("SR0054-1") validates as exact-match only.
921
+ // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
904
922
  for (const code of mentionedSpecificMembers) {
905
- if (!linkedFullCodes.has(code)) {
923
+ if (!familyLinkedCodes.has(code)) {
906
924
  missingSpecificMentionedNoFamily.add(code);
907
925
  }
908
926
  }
@@ -913,10 +931,10 @@ export default class ResultDataProvider {
913
931
  for (const code of mentionedCodes) {
914
932
  const hasSpecificSuffix = /-\d+$/.test(code);
915
933
  if (hasSpecificSuffix) {
916
- if (!linkedFullCodes.has(code)) {
934
+ if (!linkedFullCodesAcrossTestCases.has(code)) {
917
935
  missingSpecificMentionedNoFamily.add(code);
918
936
  }
919
- } else if (!linkedBaseKeys.has(baseKey)) {
937
+ } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
920
938
  missingBaseWhenFamilyUncovered.add(baseKey);
921
939
  }
922
940
  }
@@ -929,15 +947,26 @@ export default class ResultDataProvider {
929
947
  if (!baseKey) return false;
930
948
  return !mentionedBaseKeys.has(baseKey);
931
949
  });
932
- const mentionedButNotLinkedByStep = new Map<string, Set<string>>();
950
+ const mentionedButNotLinkedByStep = new Map<
951
+ string,
952
+ { baseIds: Set<string>; specificIds: Set<string> }
953
+ >();
933
954
  const appendMentionedButNotLinked = (requirementId: string, stepRef: string) => {
934
955
  const normalizedRequirementId = this.normalizeMewpRequirementCodeWithSuffix(requirementId);
935
956
  if (!normalizedRequirementId) return;
936
957
  const normalizedStepRef = String(stepRef || 'Step ?').trim() || 'Step ?';
937
958
  if (!mentionedButNotLinkedByStep.has(normalizedStepRef)) {
938
- mentionedButNotLinkedByStep.set(normalizedStepRef, new Set<string>());
959
+ mentionedButNotLinkedByStep.set(normalizedStepRef, {
960
+ baseIds: new Set<string>(),
961
+ specificIds: new Set<string>(),
962
+ });
963
+ }
964
+ const entry = mentionedButNotLinkedByStep.get(normalizedStepRef)!;
965
+ if (/-\d+$/.test(normalizedRequirementId)) {
966
+ entry.specificIds.add(normalizedRequirementId);
967
+ } else {
968
+ entry.baseIds.add(normalizedRequirementId);
939
969
  }
940
- mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
941
970
  };
942
971
 
943
972
  const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
@@ -946,7 +975,6 @@ export default class ResultDataProvider {
946
975
  const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
947
976
  a.localeCompare(b)
948
977
  );
949
- const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
950
978
  for (const code of sortedMissingSpecificMentionedNoFamily) {
951
979
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
952
980
  appendMentionedButNotLinked(code, stepRef);
@@ -955,11 +983,6 @@ export default class ResultDataProvider {
955
983
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
956
984
  appendMentionedButNotLinked(baseKey, stepRef);
957
985
  }
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
986
 
964
987
  const sortedExtraLinked = [...new Set(extraLinked)]
965
988
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
@@ -978,12 +1001,23 @@ export default class ResultDataProvider {
978
1001
  if (stepOrderA !== stepOrderB) return stepOrderA - stepOrderB;
979
1002
  return String(a[0]).localeCompare(String(b[0]));
980
1003
  })
981
- .map(([stepRef, requirementIds]) => {
982
- const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
983
- return `${stepRef}: ${groupedRequirementList}`;
1004
+ .map(([stepRef, requirementIdsByType]) => {
1005
+ const specificCodes = [...requirementIdsByType.specificIds].sort((a, b) =>
1006
+ this.compareMewpRequirementCodes(a, b)
1007
+ );
1008
+ const specificFamilies = new Set<string>(
1009
+ specificCodes.map((code) => this.toRequirementKey(code)).filter((code) => !!code)
1010
+ );
1011
+ const baseCodes = [...requirementIdsByType.baseIds]
1012
+ .filter((baseCode) => !specificFamilies.has(baseCode))
1013
+ .sort((a, b) => this.compareMewpRequirementCodes(a, b));
1014
+ const displayCodes = [...specificCodes, ...baseCodes];
1015
+ if (displayCodes.length === 0) return `${stepRef}:`;
1016
+ return `${stepRef}: ${displayCodes.join('; ')}`.trim();
984
1017
  })
985
- .join('\n');
986
- const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
1018
+ .join('\n')
1019
+ .trim();
1020
+ const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked).trim();
987
1021
  const validationStatus: 'Pass' | 'Fail' =
988
1022
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
989
1023
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -993,8 +1027,7 @@ export default class ResultDataProvider {
993
1027
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
994
1028
  `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
995
1029
  sortedMissingSpecificMentionedNoFamily.length +
996
- sortedMissingBaseWhenFamilyUncovered.length +
997
- sortedMissingFamilyMembers.length
1030
+ sortedMissingBaseWhenFamilyUncovered.length
998
1031
  } ` +
999
1032
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
1000
1033
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
@@ -2615,6 +2648,36 @@ export default class ResultDataProvider {
2615
2648
  return `SR${match[1]}`;
2616
2649
  }
2617
2650
 
2651
+ private compareMewpRequirementCodes(a: string, b: string): number {
2652
+ const normalizeComparableCode = (value: string): { base: number; hasSuffix: number; suffix: number; raw: string } => {
2653
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(value);
2654
+ const match = /^SR(\d+)(?:-(\d+))?$/i.exec(normalizedCode);
2655
+ if (!match) {
2656
+ return {
2657
+ base: Number.POSITIVE_INFINITY,
2658
+ hasSuffix: 1,
2659
+ suffix: Number.POSITIVE_INFINITY,
2660
+ raw: String(value || ''),
2661
+ };
2662
+ }
2663
+
2664
+ return {
2665
+ base: Number(match[1]),
2666
+ hasSuffix: match[2] ? 1 : 0,
2667
+ suffix: match[2] ? Number(match[2]) : -1,
2668
+ raw: normalizedCode,
2669
+ };
2670
+ };
2671
+
2672
+ const left = normalizeComparableCode(a);
2673
+ const right = normalizeComparableCode(b);
2674
+
2675
+ if (left.base !== right.base) return left.base - right.base;
2676
+ if (left.hasSuffix !== right.hasSuffix) return left.hasSuffix - right.hasSuffix;
2677
+ if (left.suffix !== right.suffix) return left.suffix - right.suffix;
2678
+ return left.raw.localeCompare(right.raw);
2679
+ }
2680
+
2618
2681
  private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2619
2682
  const byBaseKey = new Map<string, Set<string>>();
2620
2683
  for (const rawCode of codes || []) {
@@ -2628,11 +2691,12 @@ export default class ResultDataProvider {
2628
2691
  if (byBaseKey.size === 0) return '';
2629
2692
 
2630
2693
  return [...byBaseKey.entries()]
2631
- .sort((a, b) => a[0].localeCompare(b[0]))
2694
+ .sort((a, b) => this.compareMewpRequirementCodes(a[0], b[0]))
2632
2695
  .map(([baseKey, members]) => {
2633
- const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2696
+ const sortedMembers = [...members].sort((a, b) => this.compareMewpRequirementCodes(a, b));
2634
2697
  if (sortedMembers.length <= 1) return sortedMembers[0];
2635
- return `${baseKey}: ${sortedMembers.join(', ')}`;
2698
+ // Direction B display is family-level when multiple members exist.
2699
+ return baseKey;
2636
2700
  })
2637
2701
  .join('\n');
2638
2702
  }
@@ -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
  ]);
@@ -2003,6 +2003,72 @@ describe('ResultDataProvider', () => {
2003
2003
  );
2004
2004
  });
2005
2005
 
2006
+ it('should keep explicit child IDs when multiple specifically mentioned children are missing', async () => {
2007
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2008
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2009
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2010
+ {
2011
+ testPointsItems: [{ testCaseId: 111, testCaseName: 'TC 111' }],
2012
+ testCasesItems: [
2013
+ {
2014
+ workItem: {
2015
+ id: 111,
2016
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
2017
+ },
2018
+ },
2019
+ ],
2020
+ },
2021
+ ]);
2022
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2023
+ {
2024
+ workItemId: 5101,
2025
+ requirementId: 'SR0054-1',
2026
+ baseKey: 'SR0054',
2027
+ title: 'Req child 1',
2028
+ responsibility: 'ESUK',
2029
+ linkedTestCaseIds: [],
2030
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2031
+ },
2032
+ {
2033
+ workItemId: 5102,
2034
+ requirementId: 'SR0054-2',
2035
+ baseKey: 'SR0054',
2036
+ title: 'Req child 2',
2037
+ responsibility: 'ESUK',
2038
+ linkedTestCaseIds: [],
2039
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2040
+ },
2041
+ ]);
2042
+ jest
2043
+ .spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase')
2044
+ .mockResolvedValueOnce(new Map([[111, { baseKeys: new Set<string>(), fullCodes: new Set<string>() }]]));
2045
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2046
+ {
2047
+ stepId: '4',
2048
+ stepPosition: '4',
2049
+ action: '',
2050
+ expected: 'SR0054-1; SR0054-2',
2051
+ isSharedStepTitle: false,
2052
+ },
2053
+ ]);
2054
+
2055
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2056
+ '123',
2057
+ mockProjectName,
2058
+ [1]
2059
+ );
2060
+
2061
+ expect(result.rows).toHaveLength(1);
2062
+ expect(result.rows[0]).toEqual(
2063
+ expect.objectContaining({
2064
+ 'Test Case ID': 111,
2065
+ 'Mentioned but Not Linked': 'Step 4: SR0054-1; SR0054-2',
2066
+ 'Linked but Not Mentioned': '',
2067
+ 'Validation Status': 'Fail',
2068
+ })
2069
+ );
2070
+ });
2071
+
2006
2072
  it('should pass when a base SR mention is fully covered by its only linked child', async () => {
2007
2073
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2008
2074
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2077,6 +2143,115 @@ describe('ResultDataProvider', () => {
2077
2143
  );
2078
2144
  });
2079
2145
 
2146
+ it('should support cross-test-case family coverage when siblings are linked on different test cases', async () => {
2147
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2148
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2149
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2150
+ {
2151
+ testPointsItems: [
2152
+ { testCaseId: 501, testCaseName: 'TC 501 - sibling 1' },
2153
+ { testCaseId: 502, testCaseName: 'TC 502 - sibling 2' },
2154
+ ],
2155
+ testCasesItems: [
2156
+ {
2157
+ workItem: {
2158
+ id: 501,
2159
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-501\"></steps>' }],
2160
+ },
2161
+ },
2162
+ {
2163
+ workItem: {
2164
+ id: 502,
2165
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-502\"></steps>' }],
2166
+ },
2167
+ },
2168
+ ],
2169
+ },
2170
+ ]);
2171
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2172
+ {
2173
+ workItemId: 9301,
2174
+ requirementId: 'SR0054-1',
2175
+ baseKey: 'SR0054',
2176
+ title: 'SR0054 child 1',
2177
+ responsibility: 'ESUK',
2178
+ linkedTestCaseIds: [],
2179
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2180
+ },
2181
+ {
2182
+ workItemId: 9302,
2183
+ requirementId: 'SR0054-2',
2184
+ baseKey: 'SR0054',
2185
+ title: 'SR0054 child 2',
2186
+ responsibility: 'ESUK',
2187
+ linkedTestCaseIds: [],
2188
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2189
+ },
2190
+ ]);
2191
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2192
+ new Map([
2193
+ [
2194
+ 501,
2195
+ {
2196
+ baseKeys: new Set(['SR0054']),
2197
+ fullCodes: new Set(['SR0054-1']),
2198
+ },
2199
+ ],
2200
+ [
2201
+ 502,
2202
+ {
2203
+ baseKeys: new Set(['SR0054']),
2204
+ fullCodes: new Set(['SR0054-2']),
2205
+ },
2206
+ ],
2207
+ ])
2208
+ );
2209
+ jest
2210
+ .spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps')
2211
+ .mockResolvedValueOnce([
2212
+ {
2213
+ stepId: '1',
2214
+ stepPosition: '1',
2215
+ action: 'Parent mention on first test case',
2216
+ expected: 'SR0054',
2217
+ isSharedStepTitle: false,
2218
+ },
2219
+ ])
2220
+ .mockResolvedValueOnce([
2221
+ {
2222
+ stepId: '1',
2223
+ stepPosition: '1',
2224
+ action: 'Parent mention on second test case',
2225
+ expected: 'SR0054',
2226
+ isSharedStepTitle: false,
2227
+ },
2228
+ ]);
2229
+
2230
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2231
+ '123',
2232
+ mockProjectName,
2233
+ [1]
2234
+ );
2235
+
2236
+ const byTestCase = new Map<number, any>(
2237
+ result.rows.map((row: any) => [Number(row['Test Case ID']), row])
2238
+ );
2239
+ expect(byTestCase.get(501)).toEqual(
2240
+ expect.objectContaining({
2241
+ 'Mentioned but Not Linked': '',
2242
+ 'Linked but Not Mentioned': '',
2243
+ 'Validation Status': 'Pass',
2244
+ })
2245
+ );
2246
+ expect(byTestCase.get(502)).toEqual(
2247
+ expect.objectContaining({
2248
+ 'Mentioned but Not Linked': '',
2249
+ 'Linked but Not Mentioned': '',
2250
+ 'Validation Status': 'Pass',
2251
+ })
2252
+ );
2253
+ });
2254
+
2080
2255
  it('should group linked-but-not-mentioned requirements by SR family', async () => {
2081
2256
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2082
2257
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2151,7 +2326,9 @@ describe('ResultDataProvider', () => {
2151
2326
 
2152
2327
  expect(result.rows).toHaveLength(1);
2153
2328
  expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).toBe('');
2154
- expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0054: SR0054-1, SR0054-2');
2329
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0054');
2330
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).not.toContain('SR0054-1');
2331
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).not.toContain('SR0054-2');
2155
2332
  expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0100-1');
2156
2333
  });
2157
2334
 
@@ -2562,7 +2739,7 @@ describe('ResultDataProvider', () => {
2562
2739
  expect(byTestCase.get(201)).toEqual(
2563
2740
  expect.objectContaining({
2564
2741
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2565
- 'Mentioned but Not Linked': 'Step 1: SR0095-3\nSR0511: SR0511-1, SR0511-2',
2742
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3; SR0511',
2566
2743
  'Linked but Not Mentioned': 'SR8888',
2567
2744
  'Validation Status': 'Fail',
2568
2745
  })