@elisra-devops/docgen-data-provider 1.91.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.91.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",
@@ -886,8 +886,10 @@ export default class ResultDataProvider {
886
886
  }
887
887
 
888
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.
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.
891
893
  // 2) Child mention ("SR0054-1") is exact-match and checked across scoped test cases.
892
894
  const missingBaseWhenFamilyUncovered = new Set<string>();
893
895
  const missingSpecificMentionedNoFamily = new Set<string>();
@@ -900,9 +902,18 @@ export default class ResultDataProvider {
900
902
  if (familyCodes?.size) {
901
903
  const familyLinkedCodes = linkedFamilyCodesAcrossTestCases.get(baseKey) || new Set<string>();
902
904
 
903
- // Base mention ("SR0054") is satisfied by any linked member in the same family.
905
+ // Base mention ("SR0054") requires full family coverage across selected test cases.
904
906
  if (hasBaseMention) {
905
- if (familyLinkedCodes.size === 0) {
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) {
906
917
  missingBaseWhenFamilyUncovered.add(baseKey);
907
918
  }
908
919
  }
@@ -936,15 +947,26 @@ export default class ResultDataProvider {
936
947
  if (!baseKey) return false;
937
948
  return !mentionedBaseKeys.has(baseKey);
938
949
  });
939
- const mentionedButNotLinkedByStep = new Map<string, Set<string>>();
950
+ const mentionedButNotLinkedByStep = new Map<
951
+ string,
952
+ { baseIds: Set<string>; specificIds: Set<string> }
953
+ >();
940
954
  const appendMentionedButNotLinked = (requirementId: string, stepRef: string) => {
941
955
  const normalizedRequirementId = this.normalizeMewpRequirementCodeWithSuffix(requirementId);
942
956
  if (!normalizedRequirementId) return;
943
957
  const normalizedStepRef = String(stepRef || 'Step ?').trim() || 'Step ?';
944
958
  if (!mentionedButNotLinkedByStep.has(normalizedStepRef)) {
945
- 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);
946
969
  }
947
- mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
948
970
  };
949
971
 
950
972
  const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
@@ -979,8 +1001,19 @@ export default class ResultDataProvider {
979
1001
  if (stepOrderA !== stepOrderB) return stepOrderA - stepOrderB;
980
1002
  return String(a[0]).localeCompare(String(b[0]));
981
1003
  })
982
- .map(([stepRef, requirementIds]) => {
983
- return this.formatStepScopedRequirementGroups(stepRef, requirementIds);
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
1018
  .join('\n')
986
1019
  .trim();
@@ -2645,19 +2678,6 @@ export default class ResultDataProvider {
2645
2678
  return left.raw.localeCompare(right.raw);
2646
2679
  }
2647
2680
 
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
-
2661
2681
  private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2662
2682
  const byBaseKey = new Map<string, Set<string>>();
2663
2683
  for (const rawCode of codes || []) {
@@ -2675,12 +2695,7 @@ export default class ResultDataProvider {
2675
2695
  .map(([baseKey, members]) => {
2676
2696
  const sortedMembers = [...members].sort((a, b) => this.compareMewpRequirementCodes(a, b));
2677
2697
  if (sortedMembers.length <= 1) return sortedMembers[0];
2678
-
2679
- const nonBaseMembers = sortedMembers.filter((member) => member !== baseKey);
2680
- if (nonBaseMembers.length > 0) {
2681
- return `${baseKey}: ${nonBaseMembers.join(', ')}`;
2682
- }
2683
-
2698
+ // Direction B display is family-level when multiple members exist.
2684
2699
  return baseKey;
2685
2700
  })
2686
2701
  .join('\n');
@@ -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 }]);
@@ -2260,7 +2326,9 @@ describe('ResultDataProvider', () => {
2260
2326
 
2261
2327
  expect(result.rows).toHaveLength(1);
2262
2328
  expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).toBe('');
2263
- 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');
2264
2332
  expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0100-1');
2265
2333
  });
2266
2334
 
@@ -2671,7 +2739,7 @@ describe('ResultDataProvider', () => {
2671
2739
  expect(byTestCase.get(201)).toEqual(
2672
2740
  expect.objectContaining({
2673
2741
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2674
- 'Mentioned but Not Linked': 'Step 1: SR0095-3',
2742
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3; SR0511',
2675
2743
  'Linked but Not Mentioned': 'SR8888',
2676
2744
  'Validation Status': 'Fail',
2677
2745
  })